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