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