odai-dsh-agent 0.2.36 → 0.2.37
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/README.md +8 -8
- package/build/src/installer.d.mts.map +1 -1
- package/build/src/installer.mjs +0 -1
- package/build/src/installer.mjs.map +1 -1
- package/package.json +3 -3
- package/preset/odai/runtime/context-activation.d.mts +2 -11
- package/preset/odai/runtime/context-activation.d.mts.map +1 -1
- package/preset/odai/runtime/context-activation.mjs +2 -14
- package/preset/odai/runtime/context-activation.mjs.map +1 -1
- package/preset/odai/runtime/context-capability.d.mts +1 -1
- package/preset/odai/runtime/context-capability.d.mts.map +1 -1
- package/preset/odai/runtime/context-capability.mjs +1 -3
- package/preset/odai/runtime/context-capability.mjs.map +1 -1
- package/preset/odai/runtime/governance-bundle.d.mts +3 -3
- package/preset/odai/runtime/governance-bundle.d.mts.map +1 -1
- package/preset/odai/runtime/governance-bundle.mjs +2 -2
- package/preset/odai/runtime/governance-bundle.mjs.map +1 -1
- package/preset/odai/runtime/governance.d.mts +0 -1
- package/preset/odai/runtime/governance.d.mts.map +1 -1
- package/preset/odai/runtime/governance.mjs +0 -21
- package/preset/odai/runtime/governance.mjs.map +1 -1
- package/preset/odai/runtime/index.d.mts.map +1 -1
- package/preset/odai/runtime/index.mjs +4 -3
- package/preset/odai/runtime/index.mjs.map +1 -1
- package/preset/odai/runtime/prompt-runtime.d.mts +0 -2
- package/preset/odai/runtime/prompt-runtime.d.mts.map +1 -1
- package/preset/odai/runtime/prompt-runtime.mjs +3 -38
- package/preset/odai/runtime/prompt-runtime.mjs.map +1 -1
- package/preset/odai/runtime/responsibility-gap.d.mts +0 -1
- package/preset/odai/runtime/responsibility-gap.d.mts.map +1 -1
- package/preset/odai/runtime/responsibility-gap.mjs +0 -3
- package/preset/odai/runtime/responsibility-gap.mjs.map +1 -1
- package/preset/odai/runtime/runtime-config.d.mts +0 -1
- package/preset/odai/runtime/runtime-config.d.mts.map +1 -1
- package/preset/odai/runtime/runtime-config.mjs +1 -7
- package/preset/odai/runtime/runtime-config.mjs.map +1 -1
- package/preset/odai/runtime/runtime-support.d.mts +1 -7
- package/preset/odai/runtime/runtime-support.d.mts.map +1 -1
- package/preset/odai/runtime/runtime-support.mjs +9 -4
- package/preset/odai/runtime/runtime-support.mjs.map +1 -1
- package/preset/odai/runtime/runtime-types.d.mts +0 -1
- package/preset/odai/runtime/runtime-types.d.mts.map +1 -1
- package/preset/odai/runtime/semantic-memory.d.mts.map +1 -1
- package/preset/odai/runtime/semantic-memory.mjs +13 -7
- package/preset/odai/runtime/semantic-memory.mjs.map +1 -1
- package/preset/odai/runtime/skill-bundle.d.mts +2 -2
- package/preset/odai/runtime/skill-bundle.d.mts.map +1 -1
- package/preset/odai/runtime/skill-bundle.mjs +1 -1
- package/preset/odai/runtime/skill-bundle.mjs.map +1 -1
- package/preset/odai/runtime/tool-runtime.d.mts +1 -3
- package/preset/odai/runtime/tool-runtime.d.mts.map +1 -1
- package/preset/odai/runtime/tool-runtime.mjs +2 -12
- package/preset/odai/runtime/tool-runtime.mjs.map +1 -1
- package/preset/odai/skills/odai/SKILL.md +28 -17
- package/preset/odai/skills/odai/manifest.json +4 -4
- package/preset/odai/skills/odai/references/craft.md +8 -2
- package/preset/odai/skills/odai/references/dao.md +5 -4
- package/preset/odai/skills/odai/references/leverage.md +4 -4
- package/preset/odai/skills/odai/references/memory.md +20 -0
- package/preset/odai/skills/odai/references/planning.md +6 -6
- package/preset/odai/skills/odai/references/verification.md +2 -2
- package/preset/odai/skills/odai-orchestration/SKILL.md +1 -11
- package/preset/odai/skills/odai-orchestration/assets/claude-agents/agent.md +0 -2
- package/preset/odai/skills/odai-orchestration/manifest.json +3 -2
- package/preset/odai/skills/odai-orchestration/references/install.md +10 -0
- package/preset/odai/skills/odai-orchestration/references/orchestration.md +0 -6
- package/preset/odai/skills/odai-orchestration/scripts/compose-contracts.d.mts +3 -3
- package/preset/odai/skills/odai-orchestration/scripts/compose-contracts.mjs +2 -2
- package/preset/odai/skills/odai-orchestration/scripts/install-routing.mjs +100 -55
- package/preset/odai/runtime/skill-evolution.d.mts +0 -112
- package/preset/odai/runtime/skill-evolution.d.mts.map +0 -1
- package/preset/odai/runtime/skill-evolution.mjs +0 -1440
- package/preset/odai/runtime/skill-evolution.mjs.map +0 -1
- package/preset/odai/skills/odai/references/support.md +0 -23
|
@@ -1,1440 +0,0 @@
|
|
|
1
|
-
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
-
import { closeSync, existsSync, fsyncSync, lstatSync, mkdirSync, openSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync, } from "node:fs";
|
|
3
|
-
import { homedir } from "node:os";
|
|
4
|
-
import { dirname, relative, resolve } from "node:path";
|
|
5
|
-
import { ODAI_RUNTIME_CONTRACT, loadSkillBundle, readSkillBundleFile } from "./skill-bundle.mjs";
|
|
6
|
-
import { acquireOwnedStoreLock } from "./store-lock.mjs";
|
|
7
|
-
import { isUnknownRecord, sessionEvents } from "./runtime-types.mjs";
|
|
8
|
-
const STORE_SCHEMA_VERSION = 1;
|
|
9
|
-
const STATE_FILE = "state.json";
|
|
10
|
-
const METADATA_FILE = "metadata.json";
|
|
11
|
-
const DIGEST_PATTERN = /^[a-f0-9]{64}$/u;
|
|
12
|
-
const EVOLVABLE_PATH = /^(?:SKILL\.md|assets\/task-state\.md|references\/[a-z0-9-]+\.md|orchestration\/(?:SKILL\.md|assets\/routing-roles\/[a-z0-9-]+\.md|references\/[a-z0-9-]+\.md|contracts\/delegation\.md))$/u;
|
|
13
|
-
const MAX_STATE_BYTES = 1024 * 1024;
|
|
14
|
-
const MAX_METADATA_BYTES = 1024 * 1024;
|
|
15
|
-
const MAX_FILE_BYTES = 128 * 1024;
|
|
16
|
-
const MAX_TOTAL_REPLACEMENT_BYTES = 512 * 1024;
|
|
17
|
-
const MAX_OBJECTIVE_CHARS = 2_000;
|
|
18
|
-
const MAX_EVIDENCE_ITEMS = 20;
|
|
19
|
-
const MAX_EVIDENCE_CHARS = 2_000;
|
|
20
|
-
function sha256(value) {
|
|
21
|
-
return createHash("sha256").update(value).digest("hex");
|
|
22
|
-
}
|
|
23
|
-
function assertPlainObject(value, field) {
|
|
24
|
-
if (!isUnknownRecord(value)) {
|
|
25
|
-
throw new TypeError(`${field} must be an object`);
|
|
26
|
-
}
|
|
27
|
-
return value;
|
|
28
|
-
}
|
|
29
|
-
function assertExactFields(value, allowed, field) {
|
|
30
|
-
const unknown = Object.keys(value).filter((key) => !allowed.includes(key));
|
|
31
|
-
if (unknown.length > 0)
|
|
32
|
-
throw new TypeError(`${field} has unknown fields: ${unknown.join(", ")}`);
|
|
33
|
-
}
|
|
34
|
-
function nonEmptyString(value, field, maximum = Number.POSITIVE_INFINITY) {
|
|
35
|
-
if (typeof value !== "string" || value.trim() === "")
|
|
36
|
-
throw new TypeError(`${field} must be a non-empty string`);
|
|
37
|
-
const normalized = value.trim();
|
|
38
|
-
if (normalized.length > maximum)
|
|
39
|
-
throw new TypeError(`${field} exceeds ${maximum} characters`);
|
|
40
|
-
return normalized;
|
|
41
|
-
}
|
|
42
|
-
function exactDigest(value, field) {
|
|
43
|
-
if (typeof value !== "string" || !DIGEST_PATTERN.test(value)) {
|
|
44
|
-
throw new TypeError(`${field} must be a lowercase SHA-256 digest`);
|
|
45
|
-
}
|
|
46
|
-
return value;
|
|
47
|
-
}
|
|
48
|
-
function validTimestamp(value, field) {
|
|
49
|
-
const timestamp = nonEmptyString(value, field);
|
|
50
|
-
if (!Number.isFinite(Date.parse(timestamp)))
|
|
51
|
-
throw new TypeError(`${field} must be an ISO timestamp`);
|
|
52
|
-
return timestamp;
|
|
53
|
-
}
|
|
54
|
-
function utf8Text(buffer, field) {
|
|
55
|
-
if (buffer.byteLength > MAX_FILE_BYTES)
|
|
56
|
-
throw new Error(`${field} exceeds ${MAX_FILE_BYTES} bytes`);
|
|
57
|
-
try {
|
|
58
|
-
return new TextDecoder("utf-8", { fatal: true }).decode(buffer);
|
|
59
|
-
}
|
|
60
|
-
catch (error) {
|
|
61
|
-
throw new Error(`${field} must be valid UTF-8: ${error instanceof Error ? error.message : String(error)}`);
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
function assertUtf8String(value, field) {
|
|
65
|
-
if (typeof value !== "string")
|
|
66
|
-
throw new TypeError(`${field} must be a string`);
|
|
67
|
-
if (Buffer.from(value, "utf8").toString("utf8") !== value)
|
|
68
|
-
throw new TypeError(`${field} must be valid UTF-8`);
|
|
69
|
-
return value;
|
|
70
|
-
}
|
|
71
|
-
function stringList(value, field, minimum = 0) {
|
|
72
|
-
if (!Array.isArray(value) || value.length < minimum || value.length > MAX_EVIDENCE_ITEMS) {
|
|
73
|
-
throw new TypeError(`${field} must contain ${minimum}-${MAX_EVIDENCE_ITEMS} strings`);
|
|
74
|
-
}
|
|
75
|
-
return Object.freeze(value.map((item, index) => nonEmptyString(item, `${field}[${index}]`, MAX_EVIDENCE_CHARS)));
|
|
76
|
-
}
|
|
77
|
-
function safeGenerationId(value, field = "generationId") {
|
|
78
|
-
return exactDigest(value, field);
|
|
79
|
-
}
|
|
80
|
-
function safePath(root, ...segments) {
|
|
81
|
-
const target = resolve(root, ...segments);
|
|
82
|
-
const fromRoot = relative(resolve(root), target);
|
|
83
|
-
if (fromRoot === "" || fromRoot === ".." || fromRoot.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`)) {
|
|
84
|
-
throw new Error(`Odai evolution path escapes its root: ${target}`);
|
|
85
|
-
}
|
|
86
|
-
return target;
|
|
87
|
-
}
|
|
88
|
-
function assertRegularFile(path, label) {
|
|
89
|
-
const stat = lstatSync(path);
|
|
90
|
-
if (stat.isSymbolicLink() || !stat.isFile())
|
|
91
|
-
throw new Error(`${label} must be a regular file`);
|
|
92
|
-
return stat;
|
|
93
|
-
}
|
|
94
|
-
function assertRegularDirectory(path, label) {
|
|
95
|
-
const stat = lstatSync(path);
|
|
96
|
-
if (stat.isSymbolicLink() || !stat.isDirectory())
|
|
97
|
-
throw new Error(`${label} must be a regular directory`);
|
|
98
|
-
return path;
|
|
99
|
-
}
|
|
100
|
-
function ensureStoreRoot(root) {
|
|
101
|
-
if (!existsSync(root))
|
|
102
|
-
mkdirSync(root, { recursive: true, mode: 0o700 });
|
|
103
|
-
return assertRegularDirectory(root, "Odai skill evolution root");
|
|
104
|
-
}
|
|
105
|
-
function ensureStoreDirectory(root, name) {
|
|
106
|
-
ensureStoreRoot(root);
|
|
107
|
-
const path = safePath(root, name);
|
|
108
|
-
if (!existsSync(path))
|
|
109
|
-
mkdirSync(path, { mode: 0o700 });
|
|
110
|
-
return assertRegularDirectory(path, `Odai skill evolution ${name} directory`);
|
|
111
|
-
}
|
|
112
|
-
function requireStoreDirectory(root, name) {
|
|
113
|
-
ensureStoreRoot(root);
|
|
114
|
-
const path = safePath(root, name);
|
|
115
|
-
if (!existsSync(path))
|
|
116
|
-
throw new Error(`Odai skill evolution ${name} directory is missing`);
|
|
117
|
-
return assertRegularDirectory(path, `Odai skill evolution ${name} directory`);
|
|
118
|
-
}
|
|
119
|
-
function readJsonFile(path, label, maximumBytes) {
|
|
120
|
-
const stat = assertRegularFile(path, label);
|
|
121
|
-
if (stat.size > maximumBytes)
|
|
122
|
-
throw new Error(`${label} exceeds ${maximumBytes} bytes`);
|
|
123
|
-
let parsed;
|
|
124
|
-
try {
|
|
125
|
-
parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
126
|
-
}
|
|
127
|
-
catch (error) {
|
|
128
|
-
throw new Error(`cannot read ${label}: ${error instanceof Error ? error.message : String(error)}`);
|
|
129
|
-
}
|
|
130
|
-
return assertPlainObject(parsed, label);
|
|
131
|
-
}
|
|
132
|
-
function syncDirectory(path) {
|
|
133
|
-
let handle;
|
|
134
|
-
try {
|
|
135
|
-
handle = openSync(path, "r");
|
|
136
|
-
fsyncSync(handle);
|
|
137
|
-
}
|
|
138
|
-
catch {
|
|
139
|
-
// Directory fsync is unavailable on Windows; each file is still synced.
|
|
140
|
-
}
|
|
141
|
-
finally {
|
|
142
|
-
if (handle !== undefined)
|
|
143
|
-
closeSync(handle);
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
function writeDurableFile(path, content) {
|
|
147
|
-
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
148
|
-
const handle = openSync(path, "wx", 0o600);
|
|
149
|
-
try {
|
|
150
|
-
writeFileSync(handle, content);
|
|
151
|
-
fsyncSync(handle);
|
|
152
|
-
}
|
|
153
|
-
finally {
|
|
154
|
-
closeSync(handle);
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
function writeJsonAtomic(path, value) {
|
|
158
|
-
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
159
|
-
const serialized = `${JSON.stringify(value, null, 2)}\n`;
|
|
160
|
-
if (Buffer.byteLength(serialized, "utf8") > MAX_STATE_BYTES) {
|
|
161
|
-
throw new Error(`Odai skill evolution state exceeds ${MAX_STATE_BYTES} bytes`);
|
|
162
|
-
}
|
|
163
|
-
const temporary = `${path}.tmp-${process.pid}-${randomUUID()}`;
|
|
164
|
-
try {
|
|
165
|
-
writeDurableFile(temporary, serialized);
|
|
166
|
-
renameSync(temporary, path);
|
|
167
|
-
syncDirectory(dirname(path));
|
|
168
|
-
}
|
|
169
|
-
finally {
|
|
170
|
-
rmSync(temporary, { force: true });
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
function emptyState() {
|
|
174
|
-
return Object.freeze({ schemaVersion: STORE_SCHEMA_VERSION, revision: 0, history: Object.freeze([]) });
|
|
175
|
-
}
|
|
176
|
-
function normalizeHistoryEvent(value, index) {
|
|
177
|
-
const event = assertPlainObject(value, `Odai evolution state.history[${index}]`);
|
|
178
|
-
assertExactFields(event, ["revision", "action", "from", "to", "at", "evidence"], `Odai evolution state.history[${index}]`);
|
|
179
|
-
if (typeof event.revision !== "number" || !Number.isSafeInteger(event.revision) || event.revision !== index + 1) {
|
|
180
|
-
throw new TypeError(`Odai evolution state.history[${index}].revision must be ${index + 1}`);
|
|
181
|
-
}
|
|
182
|
-
if (event.action !== "activate" && event.action !== "rollback" && event.action !== "deactivate") {
|
|
183
|
-
throw new TypeError(`Odai evolution state.history[${index}].action is invalid`);
|
|
184
|
-
}
|
|
185
|
-
const from = event.from === null ? null : safeGenerationId(event.from, `Odai evolution state.history[${index}].from`);
|
|
186
|
-
const to = event.to === null ? null : safeGenerationId(event.to, `Odai evolution state.history[${index}].to`);
|
|
187
|
-
if (from === to)
|
|
188
|
-
throw new TypeError(`Odai evolution state.history[${index}] does not change the active generation`);
|
|
189
|
-
return Object.freeze({
|
|
190
|
-
revision: event.revision,
|
|
191
|
-
action: event.action,
|
|
192
|
-
from,
|
|
193
|
-
to,
|
|
194
|
-
at: validTimestamp(event.at, `Odai evolution state.history[${index}].at`),
|
|
195
|
-
evidence: stringList(event.evidence, `Odai evolution state.history[${index}].evidence`, 1),
|
|
196
|
-
});
|
|
197
|
-
}
|
|
198
|
-
export function readSkillEvolutionState(root) {
|
|
199
|
-
if (!existsSync(root))
|
|
200
|
-
return emptyState();
|
|
201
|
-
assertRegularDirectory(root, "Odai skill evolution root");
|
|
202
|
-
const path = resolve(root, STATE_FILE);
|
|
203
|
-
if (!existsSync(path))
|
|
204
|
-
return emptyState();
|
|
205
|
-
const value = readJsonFile(path, "Odai skill evolution state", MAX_STATE_BYTES);
|
|
206
|
-
assertExactFields(value, ["schemaVersion", "revision", "activeGenerationId", "history"], "Odai skill evolution state");
|
|
207
|
-
if (value.schemaVersion !== STORE_SCHEMA_VERSION) {
|
|
208
|
-
throw new TypeError(`Odai skill evolution state has unsupported schemaVersion ${String(value.schemaVersion)}`);
|
|
209
|
-
}
|
|
210
|
-
if (typeof value.revision !== "number" || !Number.isSafeInteger(value.revision) || value.revision < 0) {
|
|
211
|
-
throw new TypeError("Odai skill evolution state.revision must be a non-negative integer");
|
|
212
|
-
}
|
|
213
|
-
if (!Array.isArray(value.history) || value.history.length !== value.revision) {
|
|
214
|
-
throw new TypeError("Odai skill evolution state.history must match revision");
|
|
215
|
-
}
|
|
216
|
-
const history = Object.freeze(value.history.map(normalizeHistoryEvent));
|
|
217
|
-
let prior = null;
|
|
218
|
-
for (const [index, event] of history.entries()) {
|
|
219
|
-
if (event.from !== prior)
|
|
220
|
-
throw new TypeError(`Odai skill evolution state.history[${index}].from breaks the active-pointer chain`);
|
|
221
|
-
if (event.action === "activate" && event.to === null) {
|
|
222
|
-
throw new TypeError(`Odai skill evolution state.history[${index}].activate requires a target generation`);
|
|
223
|
-
}
|
|
224
|
-
if (event.action === "deactivate" && (event.from === null || event.to !== null)) {
|
|
225
|
-
throw new TypeError(`Odai skill evolution state.history[${index}].deactivate must clear an active generation`);
|
|
226
|
-
}
|
|
227
|
-
if (event.action === "rollback" && event.from === null) {
|
|
228
|
-
throw new TypeError(`Odai skill evolution state.history[${index}].rollback requires an active generation`);
|
|
229
|
-
}
|
|
230
|
-
prior = event.to;
|
|
231
|
-
}
|
|
232
|
-
const activeGenerationId = value.activeGenerationId === undefined
|
|
233
|
-
? undefined
|
|
234
|
-
: safeGenerationId(value.activeGenerationId, "Odai skill evolution state.activeGenerationId");
|
|
235
|
-
const expectedActive = prior ?? undefined;
|
|
236
|
-
if (activeGenerationId !== expectedActive) {
|
|
237
|
-
throw new TypeError("Odai skill evolution state.activeGenerationId does not match its history");
|
|
238
|
-
}
|
|
239
|
-
return Object.freeze({
|
|
240
|
-
schemaVersion: STORE_SCHEMA_VERSION,
|
|
241
|
-
revision: value.revision,
|
|
242
|
-
...(activeGenerationId ? { activeGenerationId } : {}),
|
|
243
|
-
history,
|
|
244
|
-
});
|
|
245
|
-
}
|
|
246
|
-
export function resolveSkillEvolutionRoot(configuredPath, env = process.env) {
|
|
247
|
-
if (configuredPath !== undefined) {
|
|
248
|
-
if (typeof configuredPath !== "string" || configuredPath.trim() === "") {
|
|
249
|
-
throw new TypeError("config.governance.evolutionRoot must be a non-empty string");
|
|
250
|
-
}
|
|
251
|
-
return resolve(configuredPath.trim());
|
|
252
|
-
}
|
|
253
|
-
const dshHome = typeof env.DSH_HOME === "string" && env.DSH_HOME.trim() !== ""
|
|
254
|
-
? resolve(env.DSH_HOME.trim())
|
|
255
|
-
: resolve(homedir(), ".dsh");
|
|
256
|
-
return resolve(dshHome, "odai", "skill-evolution");
|
|
257
|
-
}
|
|
258
|
-
export function skillEvolutionDisabled(env = process.env) {
|
|
259
|
-
return env.ODAI_DISABLE_EVOLUTION === "1";
|
|
260
|
-
}
|
|
261
|
-
function manifestValue(bundle) {
|
|
262
|
-
const { versionParts: _versionParts, ...governance } = bundle.manifest;
|
|
263
|
-
return { governance: structuredClone(governance), orchestration: structuredClone(bundle.orchestration.manifest), requiredFiles: [...bundle.requiredFiles] };
|
|
264
|
-
}
|
|
265
|
-
function snapshotBundle(bundle) {
|
|
266
|
-
const files = new Map();
|
|
267
|
-
for (const path of bundle.requiredFiles) {
|
|
268
|
-
const content = readSkillBundleFile(bundle, path);
|
|
269
|
-
if (content.byteLength > MAX_FILE_BYTES)
|
|
270
|
-
throw new Error(`Odai skill file ${path} exceeds ${MAX_FILE_BYTES} bytes`);
|
|
271
|
-
files.set(path, content);
|
|
272
|
-
}
|
|
273
|
-
return Object.freeze({ manifest: Object.freeze(manifestValue(bundle)), files });
|
|
274
|
-
}
|
|
275
|
-
function cloneSnapshot(snapshot) {
|
|
276
|
-
return { manifest: structuredClone(snapshot.manifest), files: new Map([...snapshot.files].map(([path, content]) => [path, Buffer.from(content)])) };
|
|
277
|
-
}
|
|
278
|
-
function writeBundleSnapshot(root, snapshot) {
|
|
279
|
-
mkdirSync(root, { recursive: true, mode: 0o700 });
|
|
280
|
-
writeDurableFile(resolve(root, "manifest.json"), `${JSON.stringify(snapshot.manifest.governance, null, 2)}\n`);
|
|
281
|
-
writeDurableFile(resolve(root, "orchestration/manifest.json"), `${JSON.stringify(snapshot.manifest.orchestration, null, 2)}\n`);
|
|
282
|
-
for (const [path, content] of snapshot.files)
|
|
283
|
-
writeDurableFile(resolve(root, ...path.split("/")), content);
|
|
284
|
-
syncDirectory(root);
|
|
285
|
-
}
|
|
286
|
-
function assertEvolvablePath(path, manifest, field = "path") {
|
|
287
|
-
const normalized = nonEmptyString(path, field);
|
|
288
|
-
if (!EVOLVABLE_PATH.test(normalized) || !manifest.requiredFiles.includes(normalized)) {
|
|
289
|
-
throw new TypeError(`${field} must name an existing Odai governance Markdown file`);
|
|
290
|
-
}
|
|
291
|
-
return normalized;
|
|
292
|
-
}
|
|
293
|
-
function countOccurrences(text, needle) {
|
|
294
|
-
let count = 0;
|
|
295
|
-
let offset = 0;
|
|
296
|
-
while (true) {
|
|
297
|
-
const index = text.indexOf(needle, offset);
|
|
298
|
-
if (index < 0)
|
|
299
|
-
return count;
|
|
300
|
-
count += 1;
|
|
301
|
-
offset = index + needle.length;
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
function normalizeProposedChanges(value, manifest) {
|
|
305
|
-
if (!Array.isArray(value) || value.length === 0 || value.length > 20) {
|
|
306
|
-
throw new TypeError("changes must contain 1-20 file changes");
|
|
307
|
-
}
|
|
308
|
-
const paths = new Set();
|
|
309
|
-
let totalBytes = 0;
|
|
310
|
-
return Object.freeze(value.map((raw, index) => {
|
|
311
|
-
const change = assertPlainObject(raw, `changes[${index}]`);
|
|
312
|
-
assertExactFields(change, ["path", "expectedSha256", "replacements"], `changes[${index}]`);
|
|
313
|
-
const path = assertEvolvablePath(change.path, manifest, `changes[${index}].path`);
|
|
314
|
-
if (paths.has(path))
|
|
315
|
-
throw new TypeError(`changes contains duplicate path ${path}`);
|
|
316
|
-
paths.add(path);
|
|
317
|
-
const expectedSha256 = exactDigest(change.expectedSha256, `changes[${index}].expectedSha256`);
|
|
318
|
-
if (!Array.isArray(change.replacements) || change.replacements.length === 0 || change.replacements.length > 20) {
|
|
319
|
-
throw new TypeError(`changes[${index}].replacements must contain 1-20 replacements`);
|
|
320
|
-
}
|
|
321
|
-
const replacements = Object.freeze(change.replacements.map((rawReplacement, replacementIndex) => {
|
|
322
|
-
const replacement = assertPlainObject(rawReplacement, `changes[${index}].replacements[${replacementIndex}]`);
|
|
323
|
-
assertExactFields(replacement, ["oldString", "newString"], `changes[${index}].replacements[${replacementIndex}]`);
|
|
324
|
-
const oldString = assertUtf8String(replacement.oldString, `changes[${index}].replacements[${replacementIndex}].oldString`);
|
|
325
|
-
const newString = assertUtf8String(replacement.newString, `changes[${index}].replacements[${replacementIndex}].newString`);
|
|
326
|
-
if (oldString.length === 0)
|
|
327
|
-
throw new TypeError(`changes[${index}].replacements[${replacementIndex}].oldString must not be empty`);
|
|
328
|
-
if (oldString === newString)
|
|
329
|
-
throw new TypeError(`changes[${index}].replacements[${replacementIndex}] does not change content`);
|
|
330
|
-
totalBytes += Buffer.byteLength(oldString) + Buffer.byteLength(newString);
|
|
331
|
-
if (totalBytes > MAX_TOTAL_REPLACEMENT_BYTES) {
|
|
332
|
-
throw new TypeError(`changes exceeds ${MAX_TOTAL_REPLACEMENT_BYTES} replacement bytes`);
|
|
333
|
-
}
|
|
334
|
-
return Object.freeze({ oldString, newString });
|
|
335
|
-
}));
|
|
336
|
-
return Object.freeze({ path, expectedSha256, replacements });
|
|
337
|
-
}));
|
|
338
|
-
}
|
|
339
|
-
function applyExactReplacements(text, replacements, field) {
|
|
340
|
-
let result = text;
|
|
341
|
-
for (const [index, replacement] of replacements.entries()) {
|
|
342
|
-
const count = countOccurrences(result, replacement.oldString);
|
|
343
|
-
if (count !== 1)
|
|
344
|
-
throw new Error(`${field}.replacements[${index}].oldString must match exactly once; found ${count}`);
|
|
345
|
-
result = result.replace(replacement.oldString, replacement.newString);
|
|
346
|
-
}
|
|
347
|
-
return result;
|
|
348
|
-
}
|
|
349
|
-
function mergePatchGroups(parentPatches, proposedChanges, upstreamSnapshot, effectiveSnapshot) {
|
|
350
|
-
const patches = new Map(parentPatches.map((patch) => [patch.path, {
|
|
351
|
-
path: patch.path,
|
|
352
|
-
baseSha256: patch.baseSha256,
|
|
353
|
-
replacements: patch.replacements.map((replacement) => ({ ...replacement })),
|
|
354
|
-
}]));
|
|
355
|
-
const finalSnapshot = cloneSnapshot(effectiveSnapshot);
|
|
356
|
-
for (const change of proposedChanges) {
|
|
357
|
-
const current = finalSnapshot.files.get(change.path);
|
|
358
|
-
if (!current)
|
|
359
|
-
throw new Error(`current Odai bundle is missing ${change.path}`);
|
|
360
|
-
if (sha256(current) !== change.expectedSha256)
|
|
361
|
-
throw new Error(`changes for ${change.path} use a stale file digest`);
|
|
362
|
-
const text = utf8Text(current, change.path);
|
|
363
|
-
const next = applyExactReplacements(text, change.replacements, `changes for ${change.path}`);
|
|
364
|
-
const nextBuffer = Buffer.from(next, "utf8");
|
|
365
|
-
if (nextBuffer.byteLength > MAX_FILE_BYTES)
|
|
366
|
-
throw new Error(`${change.path} exceeds ${MAX_FILE_BYTES} bytes after replacement`);
|
|
367
|
-
finalSnapshot.files.set(change.path, nextBuffer);
|
|
368
|
-
const upstreamBase = upstreamSnapshot.files.get(change.path);
|
|
369
|
-
if (!upstreamBase)
|
|
370
|
-
throw new Error(`upstream Odai bundle is missing ${change.path}`);
|
|
371
|
-
const patch = patches.get(change.path) ?? {
|
|
372
|
-
path: change.path,
|
|
373
|
-
baseSha256: sha256(upstreamBase),
|
|
374
|
-
replacements: [],
|
|
375
|
-
};
|
|
376
|
-
patch.replacements.push(...change.replacements.map((replacement) => ({ ...replacement })));
|
|
377
|
-
patches.set(change.path, patch);
|
|
378
|
-
}
|
|
379
|
-
const normalized = [];
|
|
380
|
-
for (const patch of [...patches.values()].sort((left, right) => left.path.localeCompare(right.path))) {
|
|
381
|
-
const base = upstreamSnapshot.files.get(patch.path);
|
|
382
|
-
const result = finalSnapshot.files.get(patch.path);
|
|
383
|
-
if (!base || !result)
|
|
384
|
-
throw new Error(`Odai evolution patch path disappeared: ${patch.path}`);
|
|
385
|
-
if (sha256(base) === sha256(result))
|
|
386
|
-
continue;
|
|
387
|
-
normalized.push(Object.freeze({
|
|
388
|
-
path: patch.path,
|
|
389
|
-
baseSha256: sha256(base),
|
|
390
|
-
resultSha256: sha256(result),
|
|
391
|
-
replacements: Object.freeze(patch.replacements.map((replacement) => Object.freeze({ ...replacement }))),
|
|
392
|
-
}));
|
|
393
|
-
}
|
|
394
|
-
if (normalized.length === 0)
|
|
395
|
-
return { status: "absorbed", snapshot: finalSnapshot, patches: Object.freeze([]) };
|
|
396
|
-
return { status: "candidate", snapshot: finalSnapshot, patches: Object.freeze(normalized) };
|
|
397
|
-
}
|
|
398
|
-
function normalizePatchMetadata(value, manifest) {
|
|
399
|
-
if (!Array.isArray(value) || value.length === 0 || value.length > 20) {
|
|
400
|
-
throw new TypeError("Odai evolution metadata.patches must contain 1-20 patches");
|
|
401
|
-
}
|
|
402
|
-
const paths = new Set();
|
|
403
|
-
return Object.freeze(value.map((raw, index) => {
|
|
404
|
-
const patch = assertPlainObject(raw, `Odai evolution metadata.patches[${index}]`);
|
|
405
|
-
assertExactFields(patch, ["path", "baseSha256", "resultSha256", "replacements"], `Odai evolution metadata.patches[${index}]`);
|
|
406
|
-
const path = assertEvolvablePath(patch.path, manifest, `Odai evolution metadata.patches[${index}].path`);
|
|
407
|
-
if (paths.has(path))
|
|
408
|
-
throw new TypeError(`Odai evolution metadata contains duplicate patch path ${path}`);
|
|
409
|
-
paths.add(path);
|
|
410
|
-
if (!Array.isArray(patch.replacements) || patch.replacements.length === 0 || patch.replacements.length > 100) {
|
|
411
|
-
throw new TypeError(`Odai evolution metadata.patches[${index}].replacements is invalid`);
|
|
412
|
-
}
|
|
413
|
-
return Object.freeze({
|
|
414
|
-
path,
|
|
415
|
-
baseSha256: exactDigest(patch.baseSha256, `Odai evolution metadata.patches[${index}].baseSha256`),
|
|
416
|
-
resultSha256: exactDigest(patch.resultSha256, `Odai evolution metadata.patches[${index}].resultSha256`),
|
|
417
|
-
replacements: Object.freeze(patch.replacements.map((rawReplacement, replacementIndex) => {
|
|
418
|
-
const replacement = assertPlainObject(rawReplacement, `Odai evolution metadata.patches[${index}].replacements[${replacementIndex}]`);
|
|
419
|
-
assertExactFields(replacement, ["oldString", "newString"], `Odai evolution metadata.patches[${index}].replacements[${replacementIndex}]`);
|
|
420
|
-
const oldString = assertUtf8String(replacement.oldString, `Odai evolution metadata.patches[${index}].replacements[${replacementIndex}].oldString`);
|
|
421
|
-
const newString = assertUtf8String(replacement.newString, `Odai evolution metadata.patches[${index}].replacements[${replacementIndex}].newString`);
|
|
422
|
-
if (!oldString || oldString === newString)
|
|
423
|
-
throw new TypeError(`Odai evolution metadata.patches[${index}].replacements[${replacementIndex}] is invalid`);
|
|
424
|
-
return Object.freeze({ oldString, newString });
|
|
425
|
-
})),
|
|
426
|
-
});
|
|
427
|
-
}));
|
|
428
|
-
}
|
|
429
|
-
function normalizeGenerationAuthorization(value) {
|
|
430
|
-
const authorization = assertPlainObject(value, "Odai evolution generation metadata.authorization");
|
|
431
|
-
assertExactFields(authorization, ["action", "phrase", "turn", "eventSeq", "messageId"], "Odai evolution generation metadata.authorization");
|
|
432
|
-
if (authorization.action !== "propose" && authorization.action !== "rebase") {
|
|
433
|
-
throw new TypeError("Odai evolution generation metadata.authorization.action is invalid");
|
|
434
|
-
}
|
|
435
|
-
if (typeof authorization.turn !== "number" || !Number.isSafeInteger(authorization.turn) || authorization.turn < 0) {
|
|
436
|
-
throw new TypeError("Odai evolution generation metadata.authorization.turn must be a non-negative integer");
|
|
437
|
-
}
|
|
438
|
-
if (typeof authorization.eventSeq !== "number" || !Number.isSafeInteger(authorization.eventSeq) || authorization.eventSeq < 0) {
|
|
439
|
-
throw new TypeError("Odai evolution generation metadata.authorization.eventSeq must be a non-negative integer");
|
|
440
|
-
}
|
|
441
|
-
return Object.freeze({
|
|
442
|
-
action: authorization.action,
|
|
443
|
-
phrase: nonEmptyString(authorization.phrase, "Odai evolution generation metadata.authorization.phrase", 200),
|
|
444
|
-
turn: authorization.turn,
|
|
445
|
-
eventSeq: authorization.eventSeq,
|
|
446
|
-
messageId: nonEmptyString(authorization.messageId, "Odai evolution generation metadata.authorization.messageId", 200),
|
|
447
|
-
});
|
|
448
|
-
}
|
|
449
|
-
function generationIdentity(metadata) {
|
|
450
|
-
return {
|
|
451
|
-
schemaVersion: STORE_SCHEMA_VERSION,
|
|
452
|
-
parentGenerationId: metadata.parentGenerationId ?? null,
|
|
453
|
-
createdAt: metadata.createdAt,
|
|
454
|
-
objective: metadata.objective,
|
|
455
|
-
...(metadata.authorization ? {
|
|
456
|
-
authorization: {
|
|
457
|
-
action: metadata.authorization.action,
|
|
458
|
-
phrase: metadata.authorization.phrase,
|
|
459
|
-
turn: metadata.authorization.turn,
|
|
460
|
-
eventSeq: metadata.authorization.eventSeq,
|
|
461
|
-
messageId: metadata.authorization.messageId,
|
|
462
|
-
},
|
|
463
|
-
} : {}),
|
|
464
|
-
base: {
|
|
465
|
-
source: metadata.base.source,
|
|
466
|
-
provider: metadata.base.provider,
|
|
467
|
-
skillVersion: metadata.base.skillVersion,
|
|
468
|
-
runtimeContract: metadata.base.runtimeContract,
|
|
469
|
-
digest: metadata.base.digest,
|
|
470
|
-
},
|
|
471
|
-
result: {
|
|
472
|
-
skillVersion: metadata.result.skillVersion,
|
|
473
|
-
runtimeContract: metadata.result.runtimeContract,
|
|
474
|
-
digest: metadata.result.digest,
|
|
475
|
-
},
|
|
476
|
-
patches: metadata.patches.map((patch) => ({
|
|
477
|
-
path: patch.path,
|
|
478
|
-
baseSha256: patch.baseSha256,
|
|
479
|
-
resultSha256: patch.resultSha256,
|
|
480
|
-
replacements: patch.replacements.map((replacement) => ({
|
|
481
|
-
oldString: replacement.oldString,
|
|
482
|
-
newString: replacement.newString,
|
|
483
|
-
})),
|
|
484
|
-
})),
|
|
485
|
-
};
|
|
486
|
-
}
|
|
487
|
-
function generationIdFor(metadata) {
|
|
488
|
-
return sha256(Buffer.from(JSON.stringify(generationIdentity(metadata)), "utf8"));
|
|
489
|
-
}
|
|
490
|
-
function normalizeGenerationMetadata(value, expectedId) {
|
|
491
|
-
const metadata = assertPlainObject(value, "Odai evolution generation metadata");
|
|
492
|
-
assertExactFields(metadata, ["schemaVersion", "generationId", "parentGenerationId", "createdAt", "objective", "authorization", "base", "result", "patches"], "Odai evolution generation metadata");
|
|
493
|
-
if (metadata.schemaVersion !== STORE_SCHEMA_VERSION)
|
|
494
|
-
throw new TypeError("Odai evolution generation metadata schema is unsupported");
|
|
495
|
-
const generationId = safeGenerationId(metadata.generationId, "Odai evolution generation metadata.generationId");
|
|
496
|
-
if (generationId !== expectedId)
|
|
497
|
-
throw new Error(`Odai evolution generation directory ${expectedId} contains metadata for ${generationId}`);
|
|
498
|
-
const parentGenerationId = metadata.parentGenerationId === undefined
|
|
499
|
-
? undefined
|
|
500
|
-
: safeGenerationId(metadata.parentGenerationId, "Odai evolution generation metadata.parentGenerationId");
|
|
501
|
-
if (parentGenerationId === generationId)
|
|
502
|
-
throw new Error("Odai evolution generation cannot parent itself");
|
|
503
|
-
const base = assertPlainObject(metadata.base, "Odai evolution generation metadata.base");
|
|
504
|
-
const result = assertPlainObject(metadata.result, "Odai evolution generation metadata.result");
|
|
505
|
-
assertExactFields(base, ["source", "provider", "skillVersion", "runtimeContract", "digest"], "Odai evolution generation metadata.base");
|
|
506
|
-
assertExactFields(result, ["skillVersion", "runtimeContract", "digest"], "Odai evolution generation metadata.result");
|
|
507
|
-
return {
|
|
508
|
-
schemaVersion: STORE_SCHEMA_VERSION,
|
|
509
|
-
generationId,
|
|
510
|
-
...(parentGenerationId ? { parentGenerationId } : {}),
|
|
511
|
-
createdAt: validTimestamp(metadata.createdAt, "Odai evolution generation metadata.createdAt"),
|
|
512
|
-
objective: nonEmptyString(metadata.objective, "Odai evolution generation metadata.objective", MAX_OBJECTIVE_CHARS),
|
|
513
|
-
...(metadata.authorization === undefined ? {} : { authorization: normalizeGenerationAuthorization(metadata.authorization) }),
|
|
514
|
-
base: {
|
|
515
|
-
source: nonEmptyString(base.source, "Odai evolution generation metadata.base.source"),
|
|
516
|
-
provider: nonEmptyString(base.provider, "Odai evolution generation metadata.base.provider"),
|
|
517
|
-
skillVersion: nonEmptyString(base.skillVersion, "Odai evolution generation metadata.base.skillVersion"),
|
|
518
|
-
runtimeContract: base.runtimeContract,
|
|
519
|
-
digest: exactDigest(base.digest, "Odai evolution generation metadata.base.digest"),
|
|
520
|
-
},
|
|
521
|
-
result: {
|
|
522
|
-
skillVersion: nonEmptyString(result.skillVersion, "Odai evolution generation metadata.result.skillVersion"),
|
|
523
|
-
runtimeContract: result.runtimeContract,
|
|
524
|
-
digest: exactDigest(result.digest, "Odai evolution generation metadata.result.digest"),
|
|
525
|
-
},
|
|
526
|
-
patches: metadata.patches,
|
|
527
|
-
};
|
|
528
|
-
}
|
|
529
|
-
function walkTree(root, prefix = "") {
|
|
530
|
-
const found = [];
|
|
531
|
-
for (const entry of readdirSync(root, { withFileTypes: true }).sort((left, right) => left.name.localeCompare(right.name))) {
|
|
532
|
-
const path = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
533
|
-
if (entry.isSymbolicLink())
|
|
534
|
-
throw new Error(`Odai evolution bundle contains a symbolic link: ${path}`);
|
|
535
|
-
if (entry.isDirectory())
|
|
536
|
-
found.push(...walkTree(resolve(root, entry.name), path));
|
|
537
|
-
else if (entry.isFile())
|
|
538
|
-
found.push(path);
|
|
539
|
-
else
|
|
540
|
-
throw new Error(`Odai evolution bundle contains an unsupported filesystem entry: ${path}`);
|
|
541
|
-
}
|
|
542
|
-
return found;
|
|
543
|
-
}
|
|
544
|
-
function assertExactBundleTree(root, bundle) {
|
|
545
|
-
const expected = new Set(["manifest.json", "orchestration/manifest.json", ...bundle.requiredFiles]);
|
|
546
|
-
const actual = walkTree(root);
|
|
547
|
-
for (const path of actual) {
|
|
548
|
-
if (!expected.has(path))
|
|
549
|
-
throw new Error(`Odai evolution bundle contains undeclared file ${path}`);
|
|
550
|
-
}
|
|
551
|
-
for (const path of expected) {
|
|
552
|
-
if (!actual.includes(path))
|
|
553
|
-
throw new Error(`Odai evolution bundle is missing ${path}`);
|
|
554
|
-
}
|
|
555
|
-
}
|
|
556
|
-
function generationAuthorizationProfile(baseSnapshot, resultSnapshot, patches) {
|
|
557
|
-
const reasons = new Set();
|
|
558
|
-
const protectedGovernanceFiles = new Set([
|
|
559
|
-
"SKILL.md", "orchestration/SKILL.md",
|
|
560
|
-
`orchestration/${baseSnapshot.manifest.orchestration.delegationFile}`,
|
|
561
|
-
`orchestration/${resultSnapshot.manifest.orchestration.delegationFile}`,
|
|
562
|
-
baseSnapshot.manifest.governance.referenceFiles.dao,
|
|
563
|
-
resultSnapshot.manifest.governance.referenceFiles.dao,
|
|
564
|
-
]);
|
|
565
|
-
if (JSON.stringify(baseSnapshot.manifest) !== JSON.stringify(resultSnapshot.manifest)) {
|
|
566
|
-
reasons.add("protected-composition-topology");
|
|
567
|
-
}
|
|
568
|
-
for (const path of protectedGovernanceFiles) {
|
|
569
|
-
const base = baseSnapshot.files.get(path);
|
|
570
|
-
const result = resultSnapshot.files.get(path);
|
|
571
|
-
if (!base || !result || !base.equals(result))
|
|
572
|
-
reasons.add(`protected-file:${path}`);
|
|
573
|
-
}
|
|
574
|
-
for (const patch of patches) {
|
|
575
|
-
if (patch.replacements.some((replacement) => !replacement.newString.includes(replacement.oldString))) {
|
|
576
|
-
reasons.add(`destructive-replacement:${patch.path}`);
|
|
577
|
-
}
|
|
578
|
-
}
|
|
579
|
-
const breakingReasons = Object.freeze([...reasons].sort());
|
|
580
|
-
return Object.freeze({
|
|
581
|
-
level: breakingReasons.length > 0 ? "breaking" : "standard",
|
|
582
|
-
breakingReasons,
|
|
583
|
-
});
|
|
584
|
-
}
|
|
585
|
-
function activationPhrase(generationId, level) {
|
|
586
|
-
return `ACTIVATE${level === "breaking" ? " BREAKING" : ""} ODAI EVOLUTION ${generationId}`;
|
|
587
|
-
}
|
|
588
|
-
function validateGeneration(root, generationId, ancestry = new Set()) {
|
|
589
|
-
const id = safeGenerationId(generationId);
|
|
590
|
-
if (ancestry.has(id))
|
|
591
|
-
throw new Error(`Odai evolution lineage contains a cycle at ${id}`);
|
|
592
|
-
const nextAncestry = new Set(ancestry);
|
|
593
|
-
nextAncestry.add(id);
|
|
594
|
-
requireStoreDirectory(root, "generations");
|
|
595
|
-
const generationRoot = safePath(root, "generations", id);
|
|
596
|
-
const stat = lstatSync(generationRoot);
|
|
597
|
-
if (stat.isSymbolicLink() || !stat.isDirectory())
|
|
598
|
-
throw new Error(`Odai evolution generation ${id} must be a regular directory`);
|
|
599
|
-
const children = readdirSync(generationRoot).sort();
|
|
600
|
-
if (JSON.stringify(children) !== JSON.stringify(["base", "bundle", METADATA_FILE].sort())) {
|
|
601
|
-
throw new Error(`Odai evolution generation ${id} contains undeclared entries`);
|
|
602
|
-
}
|
|
603
|
-
const rawMetadata = readJsonFile(resolve(generationRoot, METADATA_FILE), `Odai evolution generation ${id} metadata`, MAX_METADATA_BYTES);
|
|
604
|
-
const initial = normalizeGenerationMetadata(rawMetadata, id);
|
|
605
|
-
const baseBundle = loadSkillBundle(resolve(generationRoot, "base/SKILL.md"), { source: "evolution-base", provider: "odai-evolution-store", orchestrationRoot: resolve(generationRoot, "base/orchestration") });
|
|
606
|
-
const resultBundle = loadSkillBundle(resolve(generationRoot, "bundle/SKILL.md"), { source: "evolution", provider: "odai-evolution-store", orchestrationRoot: resolve(generationRoot, "bundle/orchestration") });
|
|
607
|
-
assertExactBundleTree(resolve(generationRoot, "base"), baseBundle);
|
|
608
|
-
assertExactBundleTree(resolve(generationRoot, "bundle"), resultBundle);
|
|
609
|
-
if (baseBundle.digest !== initial.base.digest)
|
|
610
|
-
throw new Error(`Odai evolution generation ${id} base digest does not match metadata`);
|
|
611
|
-
if (resultBundle.digest !== initial.result.digest) {
|
|
612
|
-
throw new Error(`Odai evolution generation ${id} result digest does not match metadata`);
|
|
613
|
-
}
|
|
614
|
-
if (JSON.stringify(manifestValue(baseBundle)) !== JSON.stringify(manifestValue(resultBundle))) {
|
|
615
|
-
throw new Error(`Odai evolution generation ${id} changes the skill manifest`);
|
|
616
|
-
}
|
|
617
|
-
if (initial.base.skillVersion !== baseBundle.manifest.skillVersion
|
|
618
|
-
|| initial.base.runtimeContract !== baseBundle.manifest.runtimeContract
|
|
619
|
-
|| initial.result.skillVersion !== resultBundle.manifest.skillVersion
|
|
620
|
-
|| initial.result.runtimeContract !== resultBundle.manifest.runtimeContract) {
|
|
621
|
-
throw new Error(`Odai evolution generation ${id} metadata does not match its manifests`);
|
|
622
|
-
}
|
|
623
|
-
const patches = normalizePatchMetadata(initial.patches, baseBundle);
|
|
624
|
-
if (generationIdFor({ ...initial, patches }) !== id) {
|
|
625
|
-
throw new Error(`Odai evolution generation ${id} identity does not cover its stored provenance`);
|
|
626
|
-
}
|
|
627
|
-
const baseSnapshot = snapshotBundle(baseBundle);
|
|
628
|
-
const replay = cloneSnapshot(baseSnapshot);
|
|
629
|
-
for (const patch of patches) {
|
|
630
|
-
const baseContent = baseSnapshot.files.get(patch.path);
|
|
631
|
-
if (!baseContent)
|
|
632
|
-
throw new Error(`Odai evolution patch path is missing from base: ${patch.path}`);
|
|
633
|
-
if (sha256(baseContent) !== patch.baseSha256)
|
|
634
|
-
throw new Error(`Odai evolution patch ${patch.path} base digest is invalid`);
|
|
635
|
-
const resultText = applyExactReplacements(utf8Text(baseContent, patch.path), patch.replacements, `Odai evolution patch ${patch.path}`);
|
|
636
|
-
const resultContent = Buffer.from(resultText, "utf8");
|
|
637
|
-
if (sha256(resultContent) !== patch.resultSha256)
|
|
638
|
-
throw new Error(`Odai evolution patch ${patch.path} result digest is invalid`);
|
|
639
|
-
replay.files.set(patch.path, resultContent);
|
|
640
|
-
}
|
|
641
|
-
const resultSnapshot = snapshotBundle(resultBundle);
|
|
642
|
-
for (const path of baseBundle.requiredFiles) {
|
|
643
|
-
const expected = replay.files.get(path);
|
|
644
|
-
const actual = resultSnapshot.files.get(path);
|
|
645
|
-
if (!expected || !actual || !expected.equals(actual))
|
|
646
|
-
throw new Error(`Odai evolution generation ${id} has an untracked change in ${path}`);
|
|
647
|
-
}
|
|
648
|
-
const authorization = generationAuthorizationProfile(baseSnapshot, resultSnapshot, patches);
|
|
649
|
-
if (initial.parentGenerationId)
|
|
650
|
-
validateGeneration(root, initial.parentGenerationId, nextAncestry);
|
|
651
|
-
return Object.freeze({
|
|
652
|
-
root: generationRoot,
|
|
653
|
-
metadata: Object.freeze({
|
|
654
|
-
...initial,
|
|
655
|
-
base: Object.freeze(initial.base),
|
|
656
|
-
result: Object.freeze(initial.result),
|
|
657
|
-
patches,
|
|
658
|
-
}),
|
|
659
|
-
baseBundle,
|
|
660
|
-
resultBundle,
|
|
661
|
-
baseSnapshot,
|
|
662
|
-
resultSnapshot,
|
|
663
|
-
authorization,
|
|
664
|
-
});
|
|
665
|
-
}
|
|
666
|
-
function generationSummary(validated) {
|
|
667
|
-
return Object.freeze({
|
|
668
|
-
generationId: validated.metadata.generationId,
|
|
669
|
-
...(validated.metadata.parentGenerationId ? { parentGenerationId: validated.metadata.parentGenerationId } : {}),
|
|
670
|
-
createdAt: validated.metadata.createdAt,
|
|
671
|
-
objective: validated.metadata.objective,
|
|
672
|
-
...(validated.metadata.authorization ? { creationAuthorization: validated.metadata.authorization } : {}),
|
|
673
|
-
baseDigest: validated.metadata.base.digest,
|
|
674
|
-
resultDigest: validated.metadata.result.digest,
|
|
675
|
-
skillVersion: validated.metadata.result.skillVersion,
|
|
676
|
-
runtimeContract: validated.metadata.result.runtimeContract,
|
|
677
|
-
changedPaths: Object.freeze(validated.metadata.patches.map((patch) => patch.path)),
|
|
678
|
-
authorizationLevel: validated.authorization.level,
|
|
679
|
-
activationPhrase: activationPhrase(validated.metadata.generationId, validated.authorization.level),
|
|
680
|
-
breakingReasons: validated.authorization.breakingReasons,
|
|
681
|
-
});
|
|
682
|
-
}
|
|
683
|
-
function listGenerations(root) {
|
|
684
|
-
if (existsSync(root))
|
|
685
|
-
assertRegularDirectory(root, "Odai skill evolution root");
|
|
686
|
-
const generationsRoot = resolve(root, "generations");
|
|
687
|
-
if (!existsSync(generationsRoot))
|
|
688
|
-
return { generations: Object.freeze([]), diagnostics: Object.freeze([]) };
|
|
689
|
-
assertRegularDirectory(generationsRoot, "Odai skill evolution generations directory");
|
|
690
|
-
const generations = [];
|
|
691
|
-
const diagnostics = [];
|
|
692
|
-
for (const entry of readdirSync(generationsRoot, { withFileTypes: true })) {
|
|
693
|
-
if (!entry.isDirectory() || !DIGEST_PATTERN.test(entry.name)) {
|
|
694
|
-
diagnostics.push(Object.freeze({ generationId: entry.name, error: "invalid generation entry" }));
|
|
695
|
-
continue;
|
|
696
|
-
}
|
|
697
|
-
try {
|
|
698
|
-
generations.push(generationSummary(validateGeneration(root, entry.name)));
|
|
699
|
-
}
|
|
700
|
-
catch (error) {
|
|
701
|
-
diagnostics.push(Object.freeze({ generationId: entry.name, error: error instanceof Error ? error.message : String(error) }));
|
|
702
|
-
}
|
|
703
|
-
}
|
|
704
|
-
generations.sort((left, right) => right.createdAt.localeCompare(left.createdAt));
|
|
705
|
-
diagnostics.sort((left, right) => left.generationId.localeCompare(right.generationId));
|
|
706
|
-
return { generations: Object.freeze(generations), diagnostics: Object.freeze(diagnostics) };
|
|
707
|
-
}
|
|
708
|
-
function createGeneration(root, upstreamBundle, finalSnapshot, patches, options) {
|
|
709
|
-
const upstreamSnapshot = snapshotBundle(upstreamBundle);
|
|
710
|
-
const generationsRoot = ensureStoreDirectory(root, "generations");
|
|
711
|
-
const temporary = safePath(root, "generations", `.tmp-${process.pid}-${randomUUID()}`);
|
|
712
|
-
try {
|
|
713
|
-
writeBundleSnapshot(resolve(temporary, "base"), upstreamSnapshot);
|
|
714
|
-
writeBundleSnapshot(resolve(temporary, "bundle"), finalSnapshot);
|
|
715
|
-
const baseBundle = loadSkillBundle(resolve(temporary, "base/SKILL.md"), { orchestrationRoot: resolve(temporary, "base/orchestration") });
|
|
716
|
-
const resultBundle = loadSkillBundle(resolve(temporary, "bundle/SKILL.md"), { orchestrationRoot: resolve(temporary, "bundle/orchestration") });
|
|
717
|
-
if (baseBundle.digest !== upstreamBundle.digest)
|
|
718
|
-
throw new Error("Odai evolution base changed while creating a candidate");
|
|
719
|
-
if (resultBundle.digest === baseBundle.digest)
|
|
720
|
-
return { status: "absorbed" };
|
|
721
|
-
const identity = {
|
|
722
|
-
schemaVersion: STORE_SCHEMA_VERSION,
|
|
723
|
-
...(options.parentGenerationId ? { parentGenerationId: options.parentGenerationId } : {}),
|
|
724
|
-
createdAt: options.now(),
|
|
725
|
-
objective: options.objective,
|
|
726
|
-
authorization: options.authorization,
|
|
727
|
-
base: {
|
|
728
|
-
source: upstreamBundle.source,
|
|
729
|
-
provider: upstreamBundle.provider,
|
|
730
|
-
skillVersion: baseBundle.manifest.skillVersion,
|
|
731
|
-
runtimeContract: baseBundle.manifest.runtimeContract,
|
|
732
|
-
digest: baseBundle.digest,
|
|
733
|
-
},
|
|
734
|
-
result: {
|
|
735
|
-
skillVersion: resultBundle.manifest.skillVersion,
|
|
736
|
-
runtimeContract: resultBundle.manifest.runtimeContract,
|
|
737
|
-
digest: resultBundle.digest,
|
|
738
|
-
},
|
|
739
|
-
patches,
|
|
740
|
-
};
|
|
741
|
-
const generationId = generationIdFor(identity);
|
|
742
|
-
const metadata = { generationId, ...identity };
|
|
743
|
-
writeDurableFile(resolve(temporary, METADATA_FILE), `${JSON.stringify(metadata, null, 2)}\n`);
|
|
744
|
-
syncDirectory(temporary);
|
|
745
|
-
const destination = safePath(root, "generations", generationId);
|
|
746
|
-
if (existsSync(destination)) {
|
|
747
|
-
const existing = validateGeneration(root, generationId);
|
|
748
|
-
return { status: "reused", generation: existing };
|
|
749
|
-
}
|
|
750
|
-
renameSync(temporary, destination);
|
|
751
|
-
syncDirectory(generationsRoot);
|
|
752
|
-
return { status: "candidate", generation: validateGeneration(root, generationId) };
|
|
753
|
-
}
|
|
754
|
-
finally {
|
|
755
|
-
rmSync(temporary, { recursive: true, force: true });
|
|
756
|
-
}
|
|
757
|
-
}
|
|
758
|
-
function appendStateChange(root, state, action, target, evidence, now) {
|
|
759
|
-
const from = state.activeGenerationId ?? null;
|
|
760
|
-
const to = target ?? null;
|
|
761
|
-
if (from === to)
|
|
762
|
-
return state;
|
|
763
|
-
const event = {
|
|
764
|
-
revision: state.revision + 1,
|
|
765
|
-
action,
|
|
766
|
-
from,
|
|
767
|
-
to,
|
|
768
|
-
at: now(),
|
|
769
|
-
evidence,
|
|
770
|
-
};
|
|
771
|
-
const next = {
|
|
772
|
-
schemaVersion: STORE_SCHEMA_VERSION,
|
|
773
|
-
revision: event.revision,
|
|
774
|
-
...(target ? { activeGenerationId: target } : {}),
|
|
775
|
-
history: [...state.history, event],
|
|
776
|
-
};
|
|
777
|
-
writeJsonAtomic(resolve(root, STATE_FILE), next);
|
|
778
|
-
return readSkillEvolutionState(root);
|
|
779
|
-
}
|
|
780
|
-
function preserveConflict(root, generation, upstreamBundle, conflicts, authorization, now) {
|
|
781
|
-
const attemptId = randomUUID();
|
|
782
|
-
const conflictsRoot = ensureStoreDirectory(root, "conflicts");
|
|
783
|
-
const temporary = safePath(root, "conflicts", `.tmp-${process.pid}-${attemptId}`);
|
|
784
|
-
const destination = safePath(root, "conflicts", attemptId);
|
|
785
|
-
try {
|
|
786
|
-
for (const path of new Set(conflicts.map((conflict) => conflict.path))) {
|
|
787
|
-
const base = generation.baseSnapshot.files.get(path);
|
|
788
|
-
const ours = generation.resultSnapshot.files.get(path);
|
|
789
|
-
const theirs = upstreamBundle.requiredFiles.includes(path) ? readSkillBundleFile(upstreamBundle, path) : undefined;
|
|
790
|
-
if (base)
|
|
791
|
-
writeDurableFile(resolve(temporary, "base", ...path.split("/")), base);
|
|
792
|
-
if (ours)
|
|
793
|
-
writeDurableFile(resolve(temporary, "ours", ...path.split("/")), ours);
|
|
794
|
-
if (theirs)
|
|
795
|
-
writeDurableFile(resolve(temporary, "theirs", ...path.split("/")), theirs);
|
|
796
|
-
}
|
|
797
|
-
const report = {
|
|
798
|
-
schemaVersion: STORE_SCHEMA_VERSION,
|
|
799
|
-
attemptId,
|
|
800
|
-
createdAt: now(),
|
|
801
|
-
generationId: generation.metadata.generationId,
|
|
802
|
-
oldBaseDigest: generation.metadata.base.digest,
|
|
803
|
-
upstreamDigest: upstreamBundle.digest,
|
|
804
|
-
authorization,
|
|
805
|
-
conflicts,
|
|
806
|
-
};
|
|
807
|
-
writeDurableFile(resolve(temporary, "report.json"), `${JSON.stringify(report, null, 2)}\n`);
|
|
808
|
-
syncDirectory(temporary);
|
|
809
|
-
renameSync(temporary, destination);
|
|
810
|
-
syncDirectory(conflictsRoot);
|
|
811
|
-
return Object.freeze(report);
|
|
812
|
-
}
|
|
813
|
-
finally {
|
|
814
|
-
rmSync(temporary, { recursive: true, force: true });
|
|
815
|
-
}
|
|
816
|
-
}
|
|
817
|
-
function rebaseGeneration(root, generation, upstreamBundle, authorization, now) {
|
|
818
|
-
if (generation.metadata.base.digest === upstreamBundle.digest)
|
|
819
|
-
return { status: "up-to-date", generation };
|
|
820
|
-
const upstreamSnapshot = snapshotBundle(upstreamBundle);
|
|
821
|
-
const result = cloneSnapshot(upstreamSnapshot);
|
|
822
|
-
const nextPatches = [];
|
|
823
|
-
const conflicts = [];
|
|
824
|
-
for (const patch of generation.metadata.patches) {
|
|
825
|
-
const upstreamContent = upstreamSnapshot.files.get(patch.path);
|
|
826
|
-
if (!upstreamContent) {
|
|
827
|
-
conflicts.push({ path: patch.path, replacement: 0, reason: "path-removed-upstream" });
|
|
828
|
-
continue;
|
|
829
|
-
}
|
|
830
|
-
let text = utf8Text(upstreamContent, patch.path);
|
|
831
|
-
const retained = [];
|
|
832
|
-
for (const [index, replacement] of patch.replacements.entries()) {
|
|
833
|
-
const oldCount = countOccurrences(text, replacement.oldString);
|
|
834
|
-
if (oldCount === 1) {
|
|
835
|
-
text = text.replace(replacement.oldString, replacement.newString);
|
|
836
|
-
retained.push(replacement);
|
|
837
|
-
continue;
|
|
838
|
-
}
|
|
839
|
-
const alreadyApplied = replacement.newString === ""
|
|
840
|
-
? oldCount === 0
|
|
841
|
-
: oldCount === 0 && countOccurrences(text, replacement.newString) === 1;
|
|
842
|
-
if (!alreadyApplied) {
|
|
843
|
-
conflicts.push({
|
|
844
|
-
path: patch.path,
|
|
845
|
-
replacement: index,
|
|
846
|
-
reason: oldCount > 1 ? "old-context-ambiguous" : "old-context-missing",
|
|
847
|
-
});
|
|
848
|
-
}
|
|
849
|
-
}
|
|
850
|
-
if (conflicts.some((conflict) => conflict.path === patch.path))
|
|
851
|
-
continue;
|
|
852
|
-
const content = Buffer.from(text, "utf8");
|
|
853
|
-
result.files.set(patch.path, content);
|
|
854
|
-
if (sha256(content) !== sha256(upstreamContent)) {
|
|
855
|
-
nextPatches.push(Object.freeze({
|
|
856
|
-
path: patch.path,
|
|
857
|
-
baseSha256: sha256(upstreamContent),
|
|
858
|
-
resultSha256: sha256(content),
|
|
859
|
-
replacements: Object.freeze(retained.map((replacement) => Object.freeze({ ...replacement }))),
|
|
860
|
-
}));
|
|
861
|
-
}
|
|
862
|
-
}
|
|
863
|
-
if (conflicts.length > 0) {
|
|
864
|
-
return { status: "conflict", report: preserveConflict(root, generation, upstreamBundle, conflicts, authorization, now) };
|
|
865
|
-
}
|
|
866
|
-
if (nextPatches.length === 0)
|
|
867
|
-
return { status: "absorbed" };
|
|
868
|
-
return createGeneration(root, upstreamBundle, result, Object.freeze(nextPatches), {
|
|
869
|
-
parentGenerationId: generation.metadata.generationId,
|
|
870
|
-
objective: `Rebase ${generation.metadata.generationId.slice(0, 12)}: ${generation.metadata.objective}`.slice(0, MAX_OBJECTIVE_CHARS),
|
|
871
|
-
authorization,
|
|
872
|
-
now,
|
|
873
|
-
});
|
|
874
|
-
}
|
|
875
|
-
function upstreamSelection(selection) {
|
|
876
|
-
return selection?.upstream ?? selection;
|
|
877
|
-
}
|
|
878
|
-
function selectionSummary(selection) {
|
|
879
|
-
return Object.freeze({
|
|
880
|
-
source: selection.bundle.source,
|
|
881
|
-
skillVersion: selection.bundle.manifest.skillVersion,
|
|
882
|
-
runtimeContract: selection.bundle.manifest.runtimeContract,
|
|
883
|
-
digest: selection.bundle.digest,
|
|
884
|
-
});
|
|
885
|
-
}
|
|
886
|
-
export function applySkillEvolutionSelection(upstream, root, options = {}) {
|
|
887
|
-
if (!upstream?.bundle)
|
|
888
|
-
throw new TypeError("an upstream Odai skill selection is required");
|
|
889
|
-
if (options.disabled === true) {
|
|
890
|
-
return Object.freeze({
|
|
891
|
-
...upstream,
|
|
892
|
-
evolution: Object.freeze({ status: "disabled", hostOverride: true }),
|
|
893
|
-
upstream,
|
|
894
|
-
});
|
|
895
|
-
}
|
|
896
|
-
let state;
|
|
897
|
-
try {
|
|
898
|
-
state = readSkillEvolutionState(root);
|
|
899
|
-
}
|
|
900
|
-
catch (error) {
|
|
901
|
-
return Object.freeze({
|
|
902
|
-
...upstream,
|
|
903
|
-
status: "fallback",
|
|
904
|
-
reasonCode: "evolution-state-invalid",
|
|
905
|
-
detail: error instanceof Error ? error.message : String(error),
|
|
906
|
-
evolution: Object.freeze({ status: "fallback", reasonCode: "evolution-state-invalid" }),
|
|
907
|
-
upstream,
|
|
908
|
-
});
|
|
909
|
-
}
|
|
910
|
-
if (!state.activeGenerationId)
|
|
911
|
-
return upstream;
|
|
912
|
-
let generation;
|
|
913
|
-
try {
|
|
914
|
-
generation = validateGeneration(root, state.activeGenerationId);
|
|
915
|
-
}
|
|
916
|
-
catch (error) {
|
|
917
|
-
return Object.freeze({
|
|
918
|
-
...upstream,
|
|
919
|
-
status: "fallback",
|
|
920
|
-
reasonCode: "evolution-generation-invalid",
|
|
921
|
-
detail: error instanceof Error ? error.message : String(error),
|
|
922
|
-
evolution: Object.freeze({
|
|
923
|
-
status: "fallback",
|
|
924
|
-
reasonCode: "evolution-generation-invalid",
|
|
925
|
-
generationId: state.activeGenerationId,
|
|
926
|
-
}),
|
|
927
|
-
upstream,
|
|
928
|
-
});
|
|
929
|
-
}
|
|
930
|
-
if (generation.metadata.base.source !== "bundled" || upstream.bundle.source !== "bundled") {
|
|
931
|
-
return Object.freeze({
|
|
932
|
-
...upstream,
|
|
933
|
-
status: "fallback",
|
|
934
|
-
reasonCode: "evolution-scope-mismatch",
|
|
935
|
-
detail: `active evolution is based on ${generation.metadata.base.source}, current upstream is ${upstream.bundle.source}`,
|
|
936
|
-
evolution: Object.freeze({
|
|
937
|
-
status: "fallback",
|
|
938
|
-
reasonCode: "evolution-scope-mismatch",
|
|
939
|
-
generationId: state.activeGenerationId,
|
|
940
|
-
}),
|
|
941
|
-
upstream,
|
|
942
|
-
});
|
|
943
|
-
}
|
|
944
|
-
if (generation.resultBundle.manifest.runtimeContract !== ODAI_RUNTIME_CONTRACT) {
|
|
945
|
-
return Object.freeze({
|
|
946
|
-
...upstream,
|
|
947
|
-
status: "fallback",
|
|
948
|
-
reasonCode: "evolution-runtime-contract-mismatch",
|
|
949
|
-
detail: `active evolution runtimeContract ${generation.resultBundle.manifest.runtimeContract} is incompatible with ${ODAI_RUNTIME_CONTRACT}`,
|
|
950
|
-
evolution: Object.freeze({
|
|
951
|
-
status: "fallback",
|
|
952
|
-
reasonCode: "evolution-runtime-contract-mismatch",
|
|
953
|
-
generationId: state.activeGenerationId,
|
|
954
|
-
}),
|
|
955
|
-
upstream,
|
|
956
|
-
});
|
|
957
|
-
}
|
|
958
|
-
const rebaseRequired = generation.metadata.base.digest !== upstream.bundle.digest;
|
|
959
|
-
return Object.freeze({
|
|
960
|
-
...upstream,
|
|
961
|
-
status: "selected",
|
|
962
|
-
reasonCode: "evolution-active",
|
|
963
|
-
bundle: generation.resultBundle,
|
|
964
|
-
evolution: Object.freeze({
|
|
965
|
-
status: "active",
|
|
966
|
-
generationId: state.activeGenerationId,
|
|
967
|
-
baseDigest: generation.metadata.base.digest,
|
|
968
|
-
upstreamDigest: upstream.bundle.digest,
|
|
969
|
-
rebaseRequired,
|
|
970
|
-
}),
|
|
971
|
-
upstream,
|
|
972
|
-
});
|
|
973
|
-
}
|
|
974
|
-
function requireController(execution) {
|
|
975
|
-
if (!execution.agent)
|
|
976
|
-
throw new Error("odai_skill_evolution requires an owning agent session");
|
|
977
|
-
const header = execution.agent.session?.header;
|
|
978
|
-
const delegationDepth = header?.delegationDepth;
|
|
979
|
-
if (header?.origin === "subagent" || (typeof delegationDepth === "number" && Number.isSafeInteger(delegationDepth) && delegationDepth > 0)) {
|
|
980
|
-
throw new Error("child agents may not inspect or change Odai skill evolution");
|
|
981
|
-
}
|
|
982
|
-
}
|
|
983
|
-
function requireHumanAuthorization(agent, phrase, action) {
|
|
984
|
-
const events = sessionEvents(agent?.session);
|
|
985
|
-
if (events.length === 0)
|
|
986
|
-
throw new Error(`current open turn must contain exactly this direct human message: ${phrase}`);
|
|
987
|
-
let boundaryIndex = -1;
|
|
988
|
-
for (let index = events.length - 1; index >= 0; index -= 1) {
|
|
989
|
-
if (events[index]?.type === "turn/start" || events[index]?.type === "turn/end") {
|
|
990
|
-
boundaryIndex = index;
|
|
991
|
-
break;
|
|
992
|
-
}
|
|
993
|
-
}
|
|
994
|
-
const boundary = events[boundaryIndex];
|
|
995
|
-
const boundaryTurn = boundary?.data?.turn;
|
|
996
|
-
if (boundary?.type !== "turn/start" || typeof boundaryTurn !== "number" || !Number.isSafeInteger(boundaryTurn) || boundaryTurn < 0) {
|
|
997
|
-
throw new Error(`current open turn must contain exactly this direct human message: ${phrase}`);
|
|
998
|
-
}
|
|
999
|
-
let event;
|
|
1000
|
-
for (let index = events.length - 1; index > boundaryIndex; index -= 1) {
|
|
1001
|
-
const candidate = events[index];
|
|
1002
|
-
if (candidate?.type === "user/message"
|
|
1003
|
-
&& candidate.data?.role === "user"
|
|
1004
|
-
&& isUnknownRecord(candidate.data?.source)
|
|
1005
|
-
&& candidate.data.source.kind === "user") {
|
|
1006
|
-
event = candidate;
|
|
1007
|
-
break;
|
|
1008
|
-
}
|
|
1009
|
-
}
|
|
1010
|
-
const message = event?.data;
|
|
1011
|
-
const block = Array.isArray(message?.content) && message.content.length === 1 ? message.content[0] : undefined;
|
|
1012
|
-
const eventSeq = event?.seq;
|
|
1013
|
-
if (!event
|
|
1014
|
-
|| typeof eventSeq !== "number"
|
|
1015
|
-
|| !Number.isSafeInteger(eventSeq)
|
|
1016
|
-
|| eventSeq < 0
|
|
1017
|
-
|| !message
|
|
1018
|
-
|| typeof message.id !== "string"
|
|
1019
|
-
|| message.id.length === 0
|
|
1020
|
-
|| message.id.length > 200
|
|
1021
|
-
|| block?.type !== "text"
|
|
1022
|
-
|| typeof block.text !== "string"
|
|
1023
|
-
|| block.text !== phrase) {
|
|
1024
|
-
throw new Error(`current open turn must contain exactly this direct human message: ${phrase}`);
|
|
1025
|
-
}
|
|
1026
|
-
return Object.freeze({
|
|
1027
|
-
action,
|
|
1028
|
-
phrase,
|
|
1029
|
-
turn: boundaryTurn,
|
|
1030
|
-
eventSeq,
|
|
1031
|
-
messageId: message.id,
|
|
1032
|
-
});
|
|
1033
|
-
}
|
|
1034
|
-
function authorizationEvidence(authorization) {
|
|
1035
|
-
return Object.freeze([
|
|
1036
|
-
`direct-human-action:${authorization.action}`,
|
|
1037
|
-
`direct-human-turn:${authorization.turn}`,
|
|
1038
|
-
`direct-human-event-seq:${authorization.eventSeq}`,
|
|
1039
|
-
`direct-human-message-id:${authorization.messageId}`,
|
|
1040
|
-
`direct-human-phrase:${authorization.phrase}`,
|
|
1041
|
-
]);
|
|
1042
|
-
}
|
|
1043
|
-
function actionAuthorizationPhrase(action, current, target) {
|
|
1044
|
-
if (action === "rebase")
|
|
1045
|
-
return `REBASE ODAI EVOLUTION ${target}`;
|
|
1046
|
-
if (action === "rollback")
|
|
1047
|
-
return `ROLLBACK ODAI EVOLUTION ${current} TO ${target ?? "BUNDLED"}`;
|
|
1048
|
-
if (action === "deactivate")
|
|
1049
|
-
return `DEACTIVATE ODAI EVOLUTION ${current}`;
|
|
1050
|
-
throw new TypeError(`unsupported Odai evolution authorization action: ${action}`);
|
|
1051
|
-
}
|
|
1052
|
-
function proposalAuthorization(objective, expectedBundleDigest, changes) {
|
|
1053
|
-
const proposalDigest = sha256(Buffer.from(JSON.stringify({
|
|
1054
|
-
objective,
|
|
1055
|
-
expectedBundleDigest,
|
|
1056
|
-
changes: changes.map((change) => ({
|
|
1057
|
-
path: change.path,
|
|
1058
|
-
expectedSha256: change.expectedSha256,
|
|
1059
|
-
replacements: change.replacements.map((replacement) => ({
|
|
1060
|
-
oldString: replacement.oldString,
|
|
1061
|
-
newString: replacement.newString,
|
|
1062
|
-
})),
|
|
1063
|
-
})),
|
|
1064
|
-
}), "utf8"));
|
|
1065
|
-
return Object.freeze({
|
|
1066
|
-
proposalDigest,
|
|
1067
|
-
proposalPhrase: `PROPOSE ODAI EVOLUTION ${proposalDigest}`,
|
|
1068
|
-
});
|
|
1069
|
-
}
|
|
1070
|
-
function assertActionArguments(args, action, allowed) {
|
|
1071
|
-
if (!args || typeof args !== "object" || Array.isArray(args))
|
|
1072
|
-
throw new TypeError("arguments must be an object");
|
|
1073
|
-
if (args.action !== action)
|
|
1074
|
-
throw new TypeError(`action must be ${action}`);
|
|
1075
|
-
assertExactFields(args, ["action", ...allowed], `${action} arguments`);
|
|
1076
|
-
}
|
|
1077
|
-
function activeSummary(root, state, currentUpstream) {
|
|
1078
|
-
if (!state.activeGenerationId)
|
|
1079
|
-
return undefined;
|
|
1080
|
-
const generation = validateGeneration(root, state.activeGenerationId);
|
|
1081
|
-
return Object.freeze({
|
|
1082
|
-
...generationSummary(generation),
|
|
1083
|
-
rebaseRequired: generation.metadata.base.digest !== currentUpstream.bundle.digest,
|
|
1084
|
-
});
|
|
1085
|
-
}
|
|
1086
|
-
function renderEvolutionResult(value) {
|
|
1087
|
-
if (value.action === "show") {
|
|
1088
|
-
if (value.status === "inactive")
|
|
1089
|
-
return "No Odai user evolution is active.";
|
|
1090
|
-
if (value.status === "disabled")
|
|
1091
|
-
return "Odai user evolution is bypassed by ODAI_DISABLE_EVOLUTION=1.";
|
|
1092
|
-
if (value.status === "fallback")
|
|
1093
|
-
return `Odai user evolution is unavailable: ${value.error}`;
|
|
1094
|
-
return value.active
|
|
1095
|
-
? `Active Odai evolution ${value.active.generationId}; rebaseRequired=${String(value.active.rebaseRequired)}.`
|
|
1096
|
-
: "No Odai user evolution is active.";
|
|
1097
|
-
}
|
|
1098
|
-
if (value.action === "inspect")
|
|
1099
|
-
return `Inspected ${value.path} from bundle ${value.bundleDigest}.`;
|
|
1100
|
-
if (value.action === "propose" && value.status === "authorization-required") {
|
|
1101
|
-
return `No candidate was written. To authorize this exact proposal, the current direct human message must be exactly: ${value.proposalPhrase}`;
|
|
1102
|
-
}
|
|
1103
|
-
if (["propose", "rebase"].includes(value.action) && ["candidate", "reused"].includes(value.status)) {
|
|
1104
|
-
return value.generation
|
|
1105
|
-
? `Created inactive Odai evolution candidate ${value.generation.generationId}. Explicit activation is still required.`
|
|
1106
|
-
: "Created an inactive Odai evolution candidate. Explicit activation is still required.";
|
|
1107
|
-
}
|
|
1108
|
-
if (value.status === "conflict")
|
|
1109
|
-
return `Odai evolution rebase conflict ${value.attemptId}; the active generation was not changed.`;
|
|
1110
|
-
if (value.status === "absorbed")
|
|
1111
|
-
return "The proposed evolution is already represented by the upstream bundle; no generation was created.";
|
|
1112
|
-
if (value.status === "up-to-date")
|
|
1113
|
-
return "The evolution generation already uses the current upstream digest.";
|
|
1114
|
-
if (value.action === "validate") {
|
|
1115
|
-
if (!value.generation)
|
|
1116
|
-
return "Odai evolution validation returned no generation.";
|
|
1117
|
-
const reasons = value.generation.breakingReasons.length > 0
|
|
1118
|
-
? ` Breaking reasons: ${value.generation.breakingReasons.join(", ")}.`
|
|
1119
|
-
: "";
|
|
1120
|
-
return `Validated Odai evolution generation ${value.generation.generationId} (${value.generation.authorizationLevel}).${reasons} To authorize activation, the latest direct human message must be exactly: ${value.generation.activationPhrase}`;
|
|
1121
|
-
}
|
|
1122
|
-
if (["activate", "rollback"].includes(value.action))
|
|
1123
|
-
return `Odai evolution ${value.activeGenerationId ?? "bundled upstream"} will become active from the next user turn.`;
|
|
1124
|
-
if (value.action === "deactivate")
|
|
1125
|
-
return "Odai user evolution is deactivated from the next user turn.";
|
|
1126
|
-
return `Odai evolution ${value.action}: ${value.status}.`;
|
|
1127
|
-
}
|
|
1128
|
-
export function createSkillEvolutionTool(root, options = {}) {
|
|
1129
|
-
const currentSelectionFor = typeof options.currentSelectionFor === "function"
|
|
1130
|
-
? options.currentSelectionFor
|
|
1131
|
-
: () => options.selection;
|
|
1132
|
-
const onChanged = typeof options.onChanged === "function" ? options.onChanged : () => { };
|
|
1133
|
-
const now = typeof options.now === "function" ? options.now : () => new Date().toISOString();
|
|
1134
|
-
const disabled = options.disabled === true;
|
|
1135
|
-
const current = (agent) => {
|
|
1136
|
-
const selection = currentSelectionFor(agent);
|
|
1137
|
-
if (!selection?.bundle)
|
|
1138
|
-
throw new Error("current Odai skill selection is unavailable");
|
|
1139
|
-
return selection;
|
|
1140
|
-
};
|
|
1141
|
-
const requireEnabled = () => {
|
|
1142
|
-
if (disabled)
|
|
1143
|
-
throw new Error("Odai skill evolution is disabled by ODAI_DISABLE_EVOLUTION=1");
|
|
1144
|
-
};
|
|
1145
|
-
return {
|
|
1146
|
-
name: "odai_skill_evolution",
|
|
1147
|
-
description: [
|
|
1148
|
-
"Inspect and manage immutable user evolution generations for governance and orchestration Markdown only when the user asks. Orchestration paths start with orchestration/.",
|
|
1149
|
-
"Every write action requires an action- and content-bound phrase as the only block in the current open turn's latest genuine user message; model text and model-supplied evidence never authorize mutation.",
|
|
1150
|
-
"Propose and rebase create inactive candidates. Either skill entry, the governance dao reference, orchestration delegation, or a replacement removing old text requires ACTIVATE BREAKING. Active-pointer changes start next turn.",
|
|
1151
|
-
"Never change manifests, scripts, runtime code, or installed package files.",
|
|
1152
|
-
].join(" "),
|
|
1153
|
-
parameters: {
|
|
1154
|
-
type: "object",
|
|
1155
|
-
additionalProperties: false,
|
|
1156
|
-
required: ["action"],
|
|
1157
|
-
properties: {
|
|
1158
|
-
action: { type: "string", enum: ["show", "inspect", "propose", "validate", "activate", "rebase", "rollback", "deactivate"] },
|
|
1159
|
-
path: { type: "string" },
|
|
1160
|
-
generationId: { type: "string" },
|
|
1161
|
-
objective: { type: "string" },
|
|
1162
|
-
expectedBundleDigest: { type: "string" },
|
|
1163
|
-
expectedUpstreamDigest: { type: "string" },
|
|
1164
|
-
changes: {
|
|
1165
|
-
type: "array",
|
|
1166
|
-
items: {
|
|
1167
|
-
type: "object",
|
|
1168
|
-
additionalProperties: false,
|
|
1169
|
-
required: ["path", "expectedSha256", "replacements"],
|
|
1170
|
-
properties: {
|
|
1171
|
-
path: { type: "string" },
|
|
1172
|
-
expectedSha256: { type: "string" },
|
|
1173
|
-
replacements: {
|
|
1174
|
-
type: "array",
|
|
1175
|
-
items: {
|
|
1176
|
-
type: "object",
|
|
1177
|
-
additionalProperties: false,
|
|
1178
|
-
required: ["oldString", "newString"],
|
|
1179
|
-
properties: { oldString: { type: "string" }, newString: { type: "string" } },
|
|
1180
|
-
},
|
|
1181
|
-
},
|
|
1182
|
-
},
|
|
1183
|
-
},
|
|
1184
|
-
},
|
|
1185
|
-
},
|
|
1186
|
-
},
|
|
1187
|
-
output: {
|
|
1188
|
-
schema: {
|
|
1189
|
-
type: "object",
|
|
1190
|
-
additionalProperties: true,
|
|
1191
|
-
required: ["action", "status"],
|
|
1192
|
-
properties: { action: { type: "string" }, status: { type: "string" } },
|
|
1193
|
-
},
|
|
1194
|
-
render(_args, value) {
|
|
1195
|
-
return [{ type: "text", text: renderEvolutionResult(value) }];
|
|
1196
|
-
},
|
|
1197
|
-
},
|
|
1198
|
-
execute(args, execution) {
|
|
1199
|
-
requireController(execution);
|
|
1200
|
-
if (!args || typeof args !== "object" || Array.isArray(args))
|
|
1201
|
-
throw new TypeError("arguments must be an object");
|
|
1202
|
-
const selection = current(execution.agent);
|
|
1203
|
-
const upstream = upstreamSelection(selection);
|
|
1204
|
-
if (args.action === "show") {
|
|
1205
|
-
assertActionArguments(args, "show", []);
|
|
1206
|
-
if (disabled)
|
|
1207
|
-
return Promise.resolve(Object.freeze({ action: "show", status: "disabled", root, upstream: selectionSummary(upstream) }));
|
|
1208
|
-
let state;
|
|
1209
|
-
try {
|
|
1210
|
-
state = readSkillEvolutionState(root);
|
|
1211
|
-
const listed = listGenerations(root);
|
|
1212
|
-
const active = activeSummary(root, state, upstream);
|
|
1213
|
-
return Promise.resolve(Object.freeze({
|
|
1214
|
-
action: "show",
|
|
1215
|
-
status: active ? "active" : "inactive",
|
|
1216
|
-
root,
|
|
1217
|
-
upstream: selectionSummary(upstream),
|
|
1218
|
-
...(active ? { active } : {}),
|
|
1219
|
-
generations: listed.generations,
|
|
1220
|
-
diagnostics: listed.diagnostics,
|
|
1221
|
-
}));
|
|
1222
|
-
}
|
|
1223
|
-
catch (error) {
|
|
1224
|
-
return Promise.resolve(Object.freeze({
|
|
1225
|
-
action: "show",
|
|
1226
|
-
status: "fallback",
|
|
1227
|
-
root,
|
|
1228
|
-
upstream: selectionSummary(upstream),
|
|
1229
|
-
error: error instanceof Error ? error.message : String(error),
|
|
1230
|
-
}));
|
|
1231
|
-
}
|
|
1232
|
-
}
|
|
1233
|
-
if (args.action === "inspect") {
|
|
1234
|
-
assertActionArguments(args, "inspect", ["path", "generationId"]);
|
|
1235
|
-
const bundle = args.generationId === undefined
|
|
1236
|
-
? selection.bundle
|
|
1237
|
-
: validateGeneration(root, safeGenerationId(args.generationId)).resultBundle;
|
|
1238
|
-
const path = assertEvolvablePath(args.path, bundle);
|
|
1239
|
-
const content = readSkillBundleFile(bundle, path);
|
|
1240
|
-
return Promise.resolve(Object.freeze({
|
|
1241
|
-
action: "inspect",
|
|
1242
|
-
status: "inspected",
|
|
1243
|
-
path,
|
|
1244
|
-
source: args.generationId ? "generation" : "effective",
|
|
1245
|
-
bundleDigest: bundle.digest,
|
|
1246
|
-
upstreamDigest: upstream.bundle.digest,
|
|
1247
|
-
sha256: sha256(content),
|
|
1248
|
-
content: utf8Text(content, path),
|
|
1249
|
-
rebaseRequired: selection.evolution?.rebaseRequired === true,
|
|
1250
|
-
}));
|
|
1251
|
-
}
|
|
1252
|
-
requireEnabled();
|
|
1253
|
-
if (args.action === "propose") {
|
|
1254
|
-
assertActionArguments(args, "propose", ["objective", "expectedBundleDigest", "changes"]);
|
|
1255
|
-
if (upstream.bundle.source !== "bundled")
|
|
1256
|
-
throw new Error("Odai evolution proposals require the bundled upstream source");
|
|
1257
|
-
if (selection.evolution?.rebaseRequired === true)
|
|
1258
|
-
throw new Error("active Odai evolution requires rebase before another proposal");
|
|
1259
|
-
const expectedBundleDigest = exactDigest(args.expectedBundleDigest, "expectedBundleDigest");
|
|
1260
|
-
if (expectedBundleDigest !== selection.bundle.digest)
|
|
1261
|
-
throw new Error("expectedBundleDigest does not match the current immutable turn snapshot");
|
|
1262
|
-
const objective = nonEmptyString(args.objective, "objective", MAX_OBJECTIVE_CHARS);
|
|
1263
|
-
const changes = normalizeProposedChanges(args.changes, selection.bundle);
|
|
1264
|
-
const proposal = proposalAuthorization(objective, expectedBundleDigest, changes);
|
|
1265
|
-
let humanAuthorization;
|
|
1266
|
-
try {
|
|
1267
|
-
humanAuthorization = requireHumanAuthorization(execution.agent, proposal.proposalPhrase, "propose");
|
|
1268
|
-
}
|
|
1269
|
-
catch {
|
|
1270
|
-
return Promise.resolve(Object.freeze({
|
|
1271
|
-
action: "propose",
|
|
1272
|
-
status: "authorization-required",
|
|
1273
|
-
proposalDigest: proposal.proposalDigest,
|
|
1274
|
-
proposalPhrase: proposal.proposalPhrase,
|
|
1275
|
-
}));
|
|
1276
|
-
}
|
|
1277
|
-
const release = acquireOwnedStoreLock(resolve(root, STATE_FILE), "Odai skill evolution store");
|
|
1278
|
-
try {
|
|
1279
|
-
const state = readSkillEvolutionState(root);
|
|
1280
|
-
if ((state.activeGenerationId ?? undefined) !== (selection.evolution?.generationId ?? undefined)) {
|
|
1281
|
-
throw new Error("Odai evolution active pointer changed; retry from a new user turn");
|
|
1282
|
-
}
|
|
1283
|
-
const parent = state.activeGenerationId ? validateGeneration(root, state.activeGenerationId) : undefined;
|
|
1284
|
-
const upstreamSnapshot = snapshotBundle(upstream.bundle);
|
|
1285
|
-
const effectiveSnapshot = snapshotBundle(selection.bundle);
|
|
1286
|
-
const merged = mergePatchGroups(parent?.metadata.patches ?? [], changes, upstreamSnapshot, effectiveSnapshot);
|
|
1287
|
-
if (merged.status === "absorbed")
|
|
1288
|
-
return Promise.resolve(Object.freeze({ action: "propose", status: "absorbed" }));
|
|
1289
|
-
const created = createGeneration(root, upstream.bundle, merged.snapshot, merged.patches, {
|
|
1290
|
-
...(parent ? { parentGenerationId: parent.metadata.generationId } : {}),
|
|
1291
|
-
objective,
|
|
1292
|
-
authorization: humanAuthorization,
|
|
1293
|
-
now,
|
|
1294
|
-
});
|
|
1295
|
-
if (created.status === "absorbed") {
|
|
1296
|
-
return Promise.resolve(Object.freeze({ action: "propose", status: "absorbed" }));
|
|
1297
|
-
}
|
|
1298
|
-
const result = Object.freeze({
|
|
1299
|
-
action: "propose",
|
|
1300
|
-
status: created.status,
|
|
1301
|
-
generation: generationSummary(created.generation),
|
|
1302
|
-
activeGenerationId: state.activeGenerationId,
|
|
1303
|
-
});
|
|
1304
|
-
onChanged(execution.agent, result);
|
|
1305
|
-
return Promise.resolve(result);
|
|
1306
|
-
}
|
|
1307
|
-
finally {
|
|
1308
|
-
release();
|
|
1309
|
-
}
|
|
1310
|
-
}
|
|
1311
|
-
if (args.action === "validate") {
|
|
1312
|
-
assertActionArguments(args, "validate", ["generationId"]);
|
|
1313
|
-
const generation = validateGeneration(root, safeGenerationId(args.generationId));
|
|
1314
|
-
return Promise.resolve(Object.freeze({ action: "validate", status: "valid", generation: generationSummary(generation) }));
|
|
1315
|
-
}
|
|
1316
|
-
if (args.action === "activate") {
|
|
1317
|
-
assertActionArguments(args, "activate", ["generationId", "expectedUpstreamDigest"]);
|
|
1318
|
-
const generationId = safeGenerationId(args.generationId);
|
|
1319
|
-
const expectedUpstreamDigest = exactDigest(args.expectedUpstreamDigest, "expectedUpstreamDigest");
|
|
1320
|
-
if (upstream.bundle.source !== "bundled" || upstream.bundle.digest !== expectedUpstreamDigest) {
|
|
1321
|
-
throw new Error("current bundled upstream does not match expectedUpstreamDigest");
|
|
1322
|
-
}
|
|
1323
|
-
const generation = validateGeneration(root, generationId);
|
|
1324
|
-
if (generation.metadata.base.digest !== upstream.bundle.digest)
|
|
1325
|
-
throw new Error("candidate must be rebased onto the current bundled upstream before activation");
|
|
1326
|
-
const expectedState = readSkillEvolutionState(root);
|
|
1327
|
-
if (expectedState.activeGenerationId === generationId) {
|
|
1328
|
-
return Promise.resolve(Object.freeze({ action: "activate", status: "already-active", activeGenerationId: generationId, requiresNextTurn: false }));
|
|
1329
|
-
}
|
|
1330
|
-
const authorization = requireHumanAuthorization(execution.agent, activationPhrase(generationId, generation.authorization.level), "activate");
|
|
1331
|
-
const release = acquireOwnedStoreLock(resolve(root, STATE_FILE), "Odai skill evolution store");
|
|
1332
|
-
try {
|
|
1333
|
-
validateGeneration(root, generationId);
|
|
1334
|
-
const state = readSkillEvolutionState(root);
|
|
1335
|
-
if (state.revision !== expectedState.revision || state.activeGenerationId !== expectedState.activeGenerationId) {
|
|
1336
|
-
throw new Error("Odai evolution active pointer changed after authorization; retry from a new user turn");
|
|
1337
|
-
}
|
|
1338
|
-
const next = appendStateChange(root, state, "activate", generationId, authorizationEvidence(authorization), now);
|
|
1339
|
-
const result = Object.freeze({ action: "activate", status: "active", activeGenerationId: next.activeGenerationId, requiresNextTurn: true });
|
|
1340
|
-
onChanged(execution.agent, result);
|
|
1341
|
-
return Promise.resolve(result);
|
|
1342
|
-
}
|
|
1343
|
-
finally {
|
|
1344
|
-
release();
|
|
1345
|
-
}
|
|
1346
|
-
}
|
|
1347
|
-
if (args.action === "rebase") {
|
|
1348
|
-
assertActionArguments(args, "rebase", ["generationId", "expectedUpstreamDigest"]);
|
|
1349
|
-
const generationId = safeGenerationId(args.generationId);
|
|
1350
|
-
const expectedUpstreamDigest = exactDigest(args.expectedUpstreamDigest, "expectedUpstreamDigest");
|
|
1351
|
-
if (upstream.bundle.source !== "bundled" || upstream.bundle.digest !== expectedUpstreamDigest) {
|
|
1352
|
-
throw new Error("current bundled upstream does not match expectedUpstreamDigest");
|
|
1353
|
-
}
|
|
1354
|
-
validateGeneration(root, generationId);
|
|
1355
|
-
const authorization = requireHumanAuthorization(execution.agent, actionAuthorizationPhrase("rebase", undefined, generationId), "rebase");
|
|
1356
|
-
const release = acquireOwnedStoreLock(resolve(root, STATE_FILE), "Odai skill evolution store");
|
|
1357
|
-
try {
|
|
1358
|
-
const lockedGeneration = validateGeneration(root, generationId);
|
|
1359
|
-
const rebased = rebaseGeneration(root, lockedGeneration, upstream.bundle, authorization, now);
|
|
1360
|
-
let result;
|
|
1361
|
-
if (rebased.status === "candidate" || rebased.status === "reused") {
|
|
1362
|
-
result = Object.freeze({ action: "rebase", status: rebased.status, generation: generationSummary(rebased.generation), activeGenerationId: readSkillEvolutionState(root).activeGenerationId });
|
|
1363
|
-
}
|
|
1364
|
-
else if (rebased.status === "conflict") {
|
|
1365
|
-
result = Object.freeze({ action: "rebase", status: "conflict", attemptId: rebased.report.attemptId, conflicts: Object.freeze(rebased.report.conflicts), activeGenerationId: readSkillEvolutionState(root).activeGenerationId });
|
|
1366
|
-
}
|
|
1367
|
-
else {
|
|
1368
|
-
result = Object.freeze({ action: "rebase", status: rebased.status, activeGenerationId: readSkillEvolutionState(root).activeGenerationId });
|
|
1369
|
-
}
|
|
1370
|
-
onChanged(execution.agent, result);
|
|
1371
|
-
return Promise.resolve(result);
|
|
1372
|
-
}
|
|
1373
|
-
finally {
|
|
1374
|
-
release();
|
|
1375
|
-
}
|
|
1376
|
-
}
|
|
1377
|
-
if (args.action === "rollback") {
|
|
1378
|
-
assertActionArguments(args, "rollback", ["generationId"]);
|
|
1379
|
-
const expectedState = readSkillEvolutionState(root);
|
|
1380
|
-
if (!expectedState.activeGenerationId)
|
|
1381
|
-
return Promise.resolve(Object.freeze({ action: "rollback", status: "inactive", requiresNextTurn: false }));
|
|
1382
|
-
let target = args.generationId === undefined ? undefined : safeGenerationId(args.generationId);
|
|
1383
|
-
if (target === undefined) {
|
|
1384
|
-
const previous = [...expectedState.history].reverse().find((event) => event.to === expectedState.activeGenerationId && event.from !== event.to);
|
|
1385
|
-
target = previous?.from ?? undefined;
|
|
1386
|
-
}
|
|
1387
|
-
if (target === expectedState.activeGenerationId) {
|
|
1388
|
-
return Promise.resolve(Object.freeze({ action: "rollback", status: "already-active", activeGenerationId: target, requiresNextTurn: false }));
|
|
1389
|
-
}
|
|
1390
|
-
if (target && !expectedState.history.some((event) => event.to === target)) {
|
|
1391
|
-
throw new Error(`rollback target ${target} was never an active Odai evolution generation`);
|
|
1392
|
-
}
|
|
1393
|
-
if (target) {
|
|
1394
|
-
const generation = validateGeneration(root, target);
|
|
1395
|
-
if (generation.resultBundle.manifest.runtimeContract !== ODAI_RUNTIME_CONTRACT) {
|
|
1396
|
-
throw new Error(`rollback target ${target} is incompatible with runtime contract ${ODAI_RUNTIME_CONTRACT}`);
|
|
1397
|
-
}
|
|
1398
|
-
}
|
|
1399
|
-
const authorization = requireHumanAuthorization(execution.agent, actionAuthorizationPhrase("rollback", expectedState.activeGenerationId, target), "rollback");
|
|
1400
|
-
const release = acquireOwnedStoreLock(resolve(root, STATE_FILE), "Odai skill evolution store");
|
|
1401
|
-
try {
|
|
1402
|
-
const state = readSkillEvolutionState(root);
|
|
1403
|
-
if (state.revision !== expectedState.revision || state.activeGenerationId !== expectedState.activeGenerationId) {
|
|
1404
|
-
throw new Error("Odai evolution active pointer changed after authorization; retry from a new user turn");
|
|
1405
|
-
}
|
|
1406
|
-
const next = appendStateChange(root, state, "rollback", target, authorizationEvidence(authorization), now);
|
|
1407
|
-
const result = Object.freeze({ action: "rollback", status: target ? "active" : "inactive", activeGenerationId: next.activeGenerationId, requiresNextTurn: true });
|
|
1408
|
-
onChanged(execution.agent, result);
|
|
1409
|
-
return Promise.resolve(result);
|
|
1410
|
-
}
|
|
1411
|
-
finally {
|
|
1412
|
-
release();
|
|
1413
|
-
}
|
|
1414
|
-
}
|
|
1415
|
-
if (args.action === "deactivate") {
|
|
1416
|
-
assertActionArguments(args, "deactivate", []);
|
|
1417
|
-
const expectedState = readSkillEvolutionState(root);
|
|
1418
|
-
if (!expectedState.activeGenerationId)
|
|
1419
|
-
return Promise.resolve(Object.freeze({ action: "deactivate", status: "inactive", requiresNextTurn: false }));
|
|
1420
|
-
const authorization = requireHumanAuthorization(execution.agent, actionAuthorizationPhrase("deactivate", expectedState.activeGenerationId), "deactivate");
|
|
1421
|
-
const release = acquireOwnedStoreLock(resolve(root, STATE_FILE), "Odai skill evolution store");
|
|
1422
|
-
try {
|
|
1423
|
-
const state = readSkillEvolutionState(root);
|
|
1424
|
-
if (state.revision !== expectedState.revision || state.activeGenerationId !== expectedState.activeGenerationId) {
|
|
1425
|
-
throw new Error("Odai evolution active pointer changed after authorization; retry from a new user turn");
|
|
1426
|
-
}
|
|
1427
|
-
appendStateChange(root, state, "deactivate", undefined, authorizationEvidence(authorization), now);
|
|
1428
|
-
const result = Object.freeze({ action: "deactivate", status: "inactive", requiresNextTurn: true });
|
|
1429
|
-
onChanged(execution.agent, result);
|
|
1430
|
-
return Promise.resolve(result);
|
|
1431
|
-
}
|
|
1432
|
-
finally {
|
|
1433
|
-
release();
|
|
1434
|
-
}
|
|
1435
|
-
}
|
|
1436
|
-
throw new TypeError("action must be show, inspect, propose, validate, activate, rebase, rollback, or deactivate");
|
|
1437
|
-
},
|
|
1438
|
-
};
|
|
1439
|
-
}
|
|
1440
|
-
//# sourceMappingURL=skill-evolution.mjs.map
|