odai-dsh-agent 0.2.0 → 0.2.2

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 (61) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +138 -134
  3. package/bin/odai-dsh-agent.mjs +108 -108
  4. package/package.json +69 -69
  5. package/preset/odai/agent.cordis.yml +260 -260
  6. package/preset/odai/preset.yml +3 -3
  7. package/preset/odai/runtime/compaction-config.mjs +388 -388
  8. package/preset/odai/runtime/context-activation.mjs +71 -0
  9. package/preset/odai/runtime/context-capability.mjs +86 -0
  10. package/preset/odai/runtime/governance.mjs +128 -121
  11. package/preset/odai/runtime/human-care.mjs +45 -0
  12. package/preset/odai/runtime/human-safety-continuity.mjs +2 -2
  13. package/preset/odai/runtime/human-safety.mjs +3 -3
  14. package/preset/odai/runtime/index.mjs +2461 -1977
  15. package/preset/odai/runtime/output-config.mjs +360 -304
  16. package/preset/odai/runtime/research-packet.mjs +150 -150
  17. package/preset/odai/runtime/responsibility-gap.mjs +8 -10
  18. package/preset/odai/runtime/responsibility-scope.mjs +199 -0
  19. package/preset/odai/runtime/role-overlays.mjs +27 -27
  20. package/preset/odai/runtime/route-card.mjs +168 -152
  21. package/preset/odai/runtime/router.mjs +683 -651
  22. package/preset/odai/runtime/routing-config.mjs +480 -436
  23. package/preset/odai/runtime/routing-context.mjs +321 -321
  24. package/preset/odai/runtime/semantic-memory-store.mjs +452 -452
  25. package/preset/odai/runtime/semantic-memory.mjs +842 -842
  26. package/preset/odai/runtime/session-compat.mjs +389 -389
  27. package/preset/odai/runtime/skill-bundle.mjs +318 -318
  28. package/preset/odai/runtime/skill-evolution.mjs +1413 -1413
  29. package/preset/odai/skills/odai/SKILL.md +69 -68
  30. package/preset/odai/skills/odai/agents/openai.yaml +4 -4
  31. package/preset/odai/skills/odai/assets/claude-agents/agent.md +11 -11
  32. package/preset/odai/skills/odai/assets/codex-agents/config.toml +11 -11
  33. package/preset/odai/skills/odai/assets/codex-agents/role.toml +5 -5
  34. package/preset/odai/skills/odai/assets/copilot-agents/agent.md +13 -13
  35. package/preset/odai/skills/odai/assets/hooks-policy.example.json +24 -24
  36. package/preset/odai/skills/odai/assets/routing-roles/controller.md +15 -15
  37. package/preset/odai/skills/odai/assets/routing-roles/executor.md +11 -11
  38. package/preset/odai/skills/odai/assets/routing-roles/frontend.md +7 -7
  39. package/preset/odai/skills/odai/assets/routing-roles/planner.md +29 -29
  40. package/preset/odai/skills/odai/assets/routing-roles/researcher.md +6 -6
  41. package/preset/odai/skills/odai/assets/routing-roles/reviewer.md +11 -11
  42. package/preset/odai/skills/odai/assets/task-state.md +14 -14
  43. package/preset/odai/skills/odai/manifest.json +37 -36
  44. package/preset/odai/skills/odai/references/care.md +26 -0
  45. package/preset/odai/skills/odai/references/craft.md +54 -54
  46. package/preset/odai/skills/odai/references/dao.md +30 -30
  47. package/preset/odai/skills/odai/references/human-safety.md +33 -54
  48. package/preset/odai/skills/odai/references/leverage.md +53 -53
  49. package/preset/odai/skills/odai/references/planning.md +82 -82
  50. package/preset/odai/skills/odai/references/support.md +36 -36
  51. package/preset/odai/skills/odai/references/verification.md +23 -23
  52. package/preset/odai/skills/odai/scripts/build-hooks.mjs +273 -273
  53. package/preset/odai/skills/odai/scripts/build-routing.mjs +274 -274
  54. package/preset/odai/skills/odai/scripts/install-routing.mjs +397 -397
  55. package/preset/odai/skills/odai/scripts/odai-hook.mjs +457 -457
  56. package/preset/odai/skills/odai/scripts/run-role.mjs +336 -336
  57. package/preset/odai/skills/odai/scripts/run-routing.mjs +1154 -1154
  58. package/preset/odai/skills/odai/scripts/verify-routing.mjs +103 -103
  59. package/scripts/verify-agent-load.mjs +257 -257
  60. package/src/dsh-version.mjs +6 -2
  61. package/src/installer.mjs +382 -382
@@ -1,318 +1,318 @@
1
- import { createHash } from "node:crypto";
2
- import { existsSync, readFileSync, realpathSync } from "node:fs";
3
- import { dirname, isAbsolute, relative, resolve } from "node:path";
4
-
5
- export const ODAI_RUNTIME_CONTRACT = 3;
6
- export const SKILL_MANIFEST_FILE = "manifest.json";
7
- export const SKILL_SOURCE_MODES = Object.freeze(["bundled", "auto", "user"]);
8
-
9
- const MANIFEST_FIELDS = new Set([
10
- "schemaVersion",
11
- "name",
12
- "skillVersion",
13
- "runtimeContract",
14
- "requiredFiles",
15
- ]);
16
- const REQUIRED_RUNTIME_FILES = Object.freeze([
17
- "SKILL.md",
18
- "assets/routing-roles/controller.md",
19
- "assets/routing-roles/researcher.md",
20
- "assets/routing-roles/planner.md",
21
- "assets/routing-roles/executor.md",
22
- "assets/routing-roles/reviewer.md",
23
- "assets/routing-roles/frontend.md",
24
- ]);
25
- const PROJECT_SOURCES = new Set(["project-dsh", "project-agents", "custom"]);
26
- const USER_SOURCES = new Set(["user-dsh", "user-agents"]);
27
- const VERSION_PATTERN = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?$/u;
28
-
29
- function assertPlainObject(value, field) {
30
- if (value === null || typeof value !== "object" || Array.isArray(value)) {
31
- throw new TypeError(`${field} must be an object`);
32
- }
33
- return value;
34
- }
35
-
36
- export function parseSkillVersion(value, field = "skillVersion") {
37
- if (typeof value !== "string") throw new TypeError(`${field} must be a string`);
38
- const match = value.match(VERSION_PATTERN);
39
- if (!match) throw new TypeError(`${field} must use SemVer 2.0.0 syntax`);
40
- const prerelease = match[4] === undefined ? [] : match[4].split(".");
41
- for (const identifier of prerelease) {
42
- if (/^\d+$/u.test(identifier) && identifier.length > 1 && identifier.startsWith("0")) {
43
- throw new TypeError(`${field} has a numeric prerelease identifier with a leading zero`);
44
- }
45
- }
46
- return Object.freeze({
47
- core: Object.freeze(match.slice(1, 4)),
48
- prerelease: Object.freeze(prerelease),
49
- });
50
- }
51
-
52
- function compareNumericIdentifier(left, right) {
53
- if (left.length < right.length) return -1;
54
- if (left.length > right.length) return 1;
55
- if (left < right) return -1;
56
- if (left > right) return 1;
57
- return 0;
58
- }
59
-
60
- function comparePrerelease(left, right) {
61
- if (left.length === 0 && right.length === 0) return 0;
62
- if (left.length === 0) return 1;
63
- if (right.length === 0) return -1;
64
- const length = Math.max(left.length, right.length);
65
- for (let index = 0; index < length; index += 1) {
66
- const leftValue = left[index];
67
- const rightValue = right[index];
68
- if (leftValue === undefined) return -1;
69
- if (rightValue === undefined) return 1;
70
- const leftNumeric = /^\d+$/u.test(leftValue);
71
- const rightNumeric = /^\d+$/u.test(rightValue);
72
- if (leftNumeric && rightNumeric) {
73
- const difference = compareNumericIdentifier(leftValue, rightValue);
74
- if (difference !== 0) return difference;
75
- continue;
76
- }
77
- if (leftNumeric !== rightNumeric) return leftNumeric ? -1 : 1;
78
- if (leftValue < rightValue) return -1;
79
- if (leftValue > rightValue) return 1;
80
- }
81
- return 0;
82
- }
83
-
84
- export function compareSkillVersions(left, right) {
85
- const leftVersion = typeof left === "string" ? parseSkillVersion(left, "left skillVersion") : left;
86
- const rightVersion = typeof right === "string" ? parseSkillVersion(right, "right skillVersion") : right;
87
- for (let index = 0; index < 3; index += 1) {
88
- const difference = compareNumericIdentifier(leftVersion.core[index], rightVersion.core[index]);
89
- if (difference !== 0) return difference;
90
- }
91
- return comparePrerelease(leftVersion.prerelease, rightVersion.prerelease);
92
- }
93
-
94
- function resolveBundleFile(root, relativePath) {
95
- if (typeof relativePath !== "string" || relativePath.trim() === "") {
96
- throw new TypeError("skill manifest requiredFiles entries must be non-empty strings");
97
- }
98
- const segments = relativePath.split("/");
99
- if (relativePath !== relativePath.trim()
100
- || relativePath.includes("\\")
101
- || isAbsolute(relativePath)
102
- || segments.some((segment) => segment === "" || segment === "." || segment === "..")) {
103
- throw new TypeError(`skill manifest contains unsafe required file ${JSON.stringify(relativePath)}`);
104
- }
105
- const target = resolve(root, ...segments);
106
- const fromRoot = relative(root, target);
107
- if (fromRoot === "" || fromRoot === ".." || fromRoot.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`)) {
108
- throw new TypeError(`skill manifest required file escapes its bundle: ${relativePath}`);
109
- }
110
- return target;
111
- }
112
-
113
- function assertRealPathInside(rootRealPath, target, label) {
114
- const targetRealPath = realpathSync(target);
115
- const fromRoot = relative(rootRealPath, targetRealPath);
116
- if (fromRoot === "" || fromRoot === ".." || fromRoot.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`)) {
117
- throw new Error(`Odai skill bundle path escapes through a symlink: ${label}`);
118
- }
119
- return targetRealPath;
120
- }
121
-
122
- export function readSkillManifest(skillRoot) {
123
- const rootRealPath = realpathSync(skillRoot);
124
- const manifestPath = resolve(skillRoot, SKILL_MANIFEST_FILE);
125
- let parsed;
126
- try {
127
- const manifestRealPath = assertRealPathInside(rootRealPath, manifestPath, SKILL_MANIFEST_FILE);
128
- parsed = JSON.parse(readFileSync(manifestRealPath, "utf8"));
129
- } catch (error) {
130
- throw new Error(`cannot read Odai skill manifest ${manifestPath}: ${error instanceof Error ? error.message : String(error)}`);
131
- }
132
- const manifest = assertPlainObject(parsed, `Odai skill manifest ${manifestPath}`);
133
- const unknown = Object.keys(manifest).filter((field) => !MANIFEST_FIELDS.has(field));
134
- if (unknown.length > 0) {
135
- throw new TypeError(`Odai skill manifest ${manifestPath} has unknown fields: ${unknown.join(", ")}`);
136
- }
137
- if (manifest.schemaVersion !== 1) {
138
- throw new TypeError(`Odai skill manifest ${manifestPath} has unsupported schemaVersion ${String(manifest.schemaVersion)}`);
139
- }
140
- if (manifest.name !== "odai") throw new TypeError(`Odai skill manifest ${manifestPath} must name odai`);
141
- const versionParts = parseSkillVersion(manifest.skillVersion, `Odai skill manifest ${manifestPath}.skillVersion`);
142
- if (!Number.isSafeInteger(manifest.runtimeContract) || manifest.runtimeContract <= 0) {
143
- throw new TypeError(`Odai skill manifest ${manifestPath}.runtimeContract must be a positive integer`);
144
- }
145
- if (!Array.isArray(manifest.requiredFiles) || manifest.requiredFiles.length === 0) {
146
- throw new TypeError(`Odai skill manifest ${manifestPath}.requiredFiles must be a non-empty array`);
147
- }
148
- const requiredFiles = manifest.requiredFiles.map((file) => {
149
- resolveBundleFile(skillRoot, file);
150
- return file;
151
- });
152
- if (new Set(requiredFiles).size !== requiredFiles.length) {
153
- throw new TypeError(`Odai skill manifest ${manifestPath}.requiredFiles contains duplicates`);
154
- }
155
- for (const required of REQUIRED_RUNTIME_FILES) {
156
- if (!requiredFiles.includes(required)) {
157
- throw new TypeError(`Odai skill manifest ${manifestPath} is missing required runtime file ${required}`);
158
- }
159
- }
160
- return Object.freeze({
161
- schemaVersion: 1,
162
- name: "odai",
163
- skillVersion: manifest.skillVersion,
164
- versionParts,
165
- runtimeContract: manifest.runtimeContract,
166
- requiredFiles: Object.freeze(requiredFiles),
167
- });
168
- }
169
-
170
- export function loadSkillBundle(skillPath, options = {}) {
171
- const entryPath = resolve(skillPath);
172
- const root = dirname(entryPath);
173
- const rootRealPath = realpathSync(root);
174
- const manifest = readSkillManifest(root);
175
- const digest = createHash("sha256");
176
- const contents = new Map();
177
- digest.update(JSON.stringify({
178
- schemaVersion: manifest.schemaVersion,
179
- name: manifest.name,
180
- skillVersion: manifest.skillVersion,
181
- runtimeContract: manifest.runtimeContract,
182
- requiredFiles: manifest.requiredFiles,
183
- }));
184
-
185
- for (const relativePath of [...manifest.requiredFiles].sort()) {
186
- const path = resolveBundleFile(root, relativePath);
187
- if (!existsSync(path)) throw new Error(`Odai skill bundle ${root} is missing ${relativePath}`);
188
- const realPath = assertRealPathInside(rootRealPath, path, relativePath);
189
- const content = readFileSync(realPath);
190
- contents.set(relativePath, content);
191
- digest.update("\0");
192
- digest.update(relativePath);
193
- digest.update("\0");
194
- digest.update(content);
195
- }
196
-
197
- const skillText = contents.get("SKILL.md").toString("utf8").trim();
198
- if (!skillText) throw new Error(`Odai canonical skill is empty: ${entryPath}`);
199
- const frontmatter = skillText.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/u)?.[1];
200
- if (!frontmatter || !/^name:\s*odai\s*$/mu.test(frontmatter)) {
201
- throw new Error(`Odai canonical skill entry does not declare name odai: ${entryPath}`);
202
- }
203
-
204
- const roleContracts = Object.freeze(Object.fromEntries(["controller", "researcher", "planner", "executor", "reviewer", "frontend"].map((role) => {
205
- const relativePath = `assets/routing-roles/${role}.md`;
206
- const text = contents.get(relativePath).toString("utf8").trim();
207
- if (!text) throw new Error(`Odai canonical ${role} role is unavailable: ${resolve(root, relativePath)}`);
208
- return [role, text];
209
- })));
210
- const referenceContracts = Object.freeze(Object.fromEntries(["craft"].map((reference) => {
211
- const relativePath = `references/${reference}.md`;
212
- const text = contents.get(relativePath)?.toString("utf8").trim();
213
- if (!text) throw new Error(`Odai canonical ${reference} reference is unavailable: ${resolve(root, relativePath)}`);
214
- return [reference, text];
215
- })));
216
- const fileContents = Object.freeze(Object.fromEntries(
217
- [...contents].map(([relativePath, content]) => [relativePath, content.toString("base64")]),
218
- ));
219
-
220
- const bundle = {
221
- path: entryPath,
222
- root,
223
- source: typeof options.source === "string" && options.source ? options.source : "bundled",
224
- provider: typeof options.provider === "string" && options.provider ? options.provider : "odai-dsh-runtime",
225
- manifest,
226
- skillText,
227
- roleContracts,
228
- referenceContracts,
229
- digest: digest.digest("hex"),
230
- };
231
- Object.defineProperty(bundle, "fileContents", { value: fileContents, enumerable: false });
232
- return Object.freeze(bundle);
233
- }
234
-
235
- export function readSkillBundleFile(bundle, relativePath) {
236
- if (!bundle || typeof bundle !== "object" || !bundle.manifest?.requiredFiles?.includes(relativePath)) {
237
- throw new TypeError(`unknown Odai skill bundle file: ${String(relativePath)}`);
238
- }
239
- const encoded = bundle.fileContents?.[relativePath];
240
- if (typeof encoded !== "string") throw new Error(`Odai skill bundle snapshot is missing ${relativePath}`);
241
- return Buffer.from(encoded, "base64");
242
- }
243
-
244
- function fallbackSelection(mode, bundled, reasonCode, detail, candidate) {
245
- return Object.freeze({
246
- mode,
247
- status: "fallback",
248
- reasonCode,
249
- detail,
250
- bundle: bundled,
251
- ...(candidate ? { candidate } : {}),
252
- });
253
- }
254
-
255
- function selected(mode, bundle, reasonCode, candidate) {
256
- return Object.freeze({
257
- mode,
258
- status: "selected",
259
- reasonCode,
260
- bundle,
261
- ...(candidate ? { candidate } : {}),
262
- });
263
- }
264
-
265
- export function chooseSkillBundle({ mode, bundled, candidate, candidateError } = {}) {
266
- if (!SKILL_SOURCE_MODES.includes(mode)) throw new TypeError(`unknown Odai skill source mode: ${String(mode)}`);
267
- if (!bundled) throw new TypeError("bundled Odai skill is required");
268
- if (mode === "bundled") return selected(mode, bundled, "bundled-configured");
269
- if (candidateError) {
270
- return fallbackSelection(mode, bundled, "external-invalid", candidateError instanceof Error ? candidateError.message : String(candidateError));
271
- }
272
- if (!candidate) {
273
- return mode === "auto"
274
- ? selected(mode, bundled, "external-not-installed")
275
- : fallbackSelection(mode, bundled, "user-source-missing", "no compatible user-level Odai skill is installed");
276
- }
277
- if (candidate.manifest.runtimeContract !== ODAI_RUNTIME_CONTRACT) {
278
- return fallbackSelection(
279
- mode,
280
- bundled,
281
- "runtime-contract-mismatch",
282
- `candidate runtimeContract ${candidate.manifest.runtimeContract} is incompatible with runtime contract ${ODAI_RUNTIME_CONTRACT}`,
283
- candidate,
284
- );
285
- }
286
-
287
- const versionOrder = compareSkillVersions(candidate.manifest.versionParts, bundled.manifest.versionParts);
288
- if (versionOrder === 0) {
289
- if (candidate.digest === bundled.digest) return selected(mode, bundled, "external-equivalent", candidate);
290
- return fallbackSelection(
291
- mode,
292
- bundled,
293
- "same-version-content-conflict",
294
- `candidate ${candidate.manifest.skillVersion} differs from the bundled content with the same version`,
295
- candidate,
296
- );
297
- }
298
-
299
- if (mode === "user") {
300
- if (!USER_SOURCES.has(candidate.source) && candidate.source !== "custom") {
301
- return fallbackSelection(mode, bundled, "user-source-invalid", `source ${candidate.source} is not user-level`, candidate);
302
- }
303
- return selected(mode, candidate, "user-configured", candidate);
304
- }
305
- if (PROJECT_SOURCES.has(candidate.source)) return selected(mode, candidate, "project-scope-override", candidate);
306
- if (USER_SOURCES.has(candidate.source)) {
307
- return versionOrder > 0
308
- ? selected(mode, candidate, "newer-user-skill", candidate)
309
- : fallbackSelection(
310
- mode,
311
- bundled,
312
- "user-skill-older",
313
- `candidate ${candidate.manifest.skillVersion} is older than bundled ${bundled.manifest.skillVersion}`,
314
- candidate,
315
- );
316
- }
317
- return fallbackSelection(mode, bundled, "external-source-unsupported", `source ${candidate.source} cannot provide Odai governance`, candidate);
318
- }
1
+ import { createHash } from "node:crypto";
2
+ import { existsSync, readFileSync, realpathSync } from "node:fs";
3
+ import { dirname, isAbsolute, relative, resolve } from "node:path";
4
+
5
+ export const ODAI_RUNTIME_CONTRACT = 3;
6
+ export const SKILL_MANIFEST_FILE = "manifest.json";
7
+ export const SKILL_SOURCE_MODES = Object.freeze(["bundled", "auto", "user"]);
8
+
9
+ const MANIFEST_FIELDS = new Set([
10
+ "schemaVersion",
11
+ "name",
12
+ "skillVersion",
13
+ "runtimeContract",
14
+ "requiredFiles",
15
+ ]);
16
+ const REQUIRED_RUNTIME_FILES = Object.freeze([
17
+ "SKILL.md",
18
+ "assets/routing-roles/controller.md",
19
+ "assets/routing-roles/researcher.md",
20
+ "assets/routing-roles/planner.md",
21
+ "assets/routing-roles/executor.md",
22
+ "assets/routing-roles/reviewer.md",
23
+ "assets/routing-roles/frontend.md",
24
+ ]);
25
+ const PROJECT_SOURCES = new Set(["project-dsh", "project-agents", "custom"]);
26
+ const USER_SOURCES = new Set(["user-dsh", "user-agents"]);
27
+ const VERSION_PATTERN = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?$/u;
28
+
29
+ function assertPlainObject(value, field) {
30
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
31
+ throw new TypeError(`${field} must be an object`);
32
+ }
33
+ return value;
34
+ }
35
+
36
+ export function parseSkillVersion(value, field = "skillVersion") {
37
+ if (typeof value !== "string") throw new TypeError(`${field} must be a string`);
38
+ const match = value.match(VERSION_PATTERN);
39
+ if (!match) throw new TypeError(`${field} must use SemVer 2.0.0 syntax`);
40
+ const prerelease = match[4] === undefined ? [] : match[4].split(".");
41
+ for (const identifier of prerelease) {
42
+ if (/^\d+$/u.test(identifier) && identifier.length > 1 && identifier.startsWith("0")) {
43
+ throw new TypeError(`${field} has a numeric prerelease identifier with a leading zero`);
44
+ }
45
+ }
46
+ return Object.freeze({
47
+ core: Object.freeze(match.slice(1, 4)),
48
+ prerelease: Object.freeze(prerelease),
49
+ });
50
+ }
51
+
52
+ function compareNumericIdentifier(left, right) {
53
+ if (left.length < right.length) return -1;
54
+ if (left.length > right.length) return 1;
55
+ if (left < right) return -1;
56
+ if (left > right) return 1;
57
+ return 0;
58
+ }
59
+
60
+ function comparePrerelease(left, right) {
61
+ if (left.length === 0 && right.length === 0) return 0;
62
+ if (left.length === 0) return 1;
63
+ if (right.length === 0) return -1;
64
+ const length = Math.max(left.length, right.length);
65
+ for (let index = 0; index < length; index += 1) {
66
+ const leftValue = left[index];
67
+ const rightValue = right[index];
68
+ if (leftValue === undefined) return -1;
69
+ if (rightValue === undefined) return 1;
70
+ const leftNumeric = /^\d+$/u.test(leftValue);
71
+ const rightNumeric = /^\d+$/u.test(rightValue);
72
+ if (leftNumeric && rightNumeric) {
73
+ const difference = compareNumericIdentifier(leftValue, rightValue);
74
+ if (difference !== 0) return difference;
75
+ continue;
76
+ }
77
+ if (leftNumeric !== rightNumeric) return leftNumeric ? -1 : 1;
78
+ if (leftValue < rightValue) return -1;
79
+ if (leftValue > rightValue) return 1;
80
+ }
81
+ return 0;
82
+ }
83
+
84
+ export function compareSkillVersions(left, right) {
85
+ const leftVersion = typeof left === "string" ? parseSkillVersion(left, "left skillVersion") : left;
86
+ const rightVersion = typeof right === "string" ? parseSkillVersion(right, "right skillVersion") : right;
87
+ for (let index = 0; index < 3; index += 1) {
88
+ const difference = compareNumericIdentifier(leftVersion.core[index], rightVersion.core[index]);
89
+ if (difference !== 0) return difference;
90
+ }
91
+ return comparePrerelease(leftVersion.prerelease, rightVersion.prerelease);
92
+ }
93
+
94
+ function resolveBundleFile(root, relativePath) {
95
+ if (typeof relativePath !== "string" || relativePath.trim() === "") {
96
+ throw new TypeError("skill manifest requiredFiles entries must be non-empty strings");
97
+ }
98
+ const segments = relativePath.split("/");
99
+ if (relativePath !== relativePath.trim()
100
+ || relativePath.includes("\\")
101
+ || isAbsolute(relativePath)
102
+ || segments.some((segment) => segment === "" || segment === "." || segment === "..")) {
103
+ throw new TypeError(`skill manifest contains unsafe required file ${JSON.stringify(relativePath)}`);
104
+ }
105
+ const target = resolve(root, ...segments);
106
+ const fromRoot = relative(root, target);
107
+ if (fromRoot === "" || fromRoot === ".." || fromRoot.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`)) {
108
+ throw new TypeError(`skill manifest required file escapes its bundle: ${relativePath}`);
109
+ }
110
+ return target;
111
+ }
112
+
113
+ function assertRealPathInside(rootRealPath, target, label) {
114
+ const targetRealPath = realpathSync(target);
115
+ const fromRoot = relative(rootRealPath, targetRealPath);
116
+ if (fromRoot === "" || fromRoot === ".." || fromRoot.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`)) {
117
+ throw new Error(`Odai skill bundle path escapes through a symlink: ${label}`);
118
+ }
119
+ return targetRealPath;
120
+ }
121
+
122
+ export function readSkillManifest(skillRoot) {
123
+ const rootRealPath = realpathSync(skillRoot);
124
+ const manifestPath = resolve(skillRoot, SKILL_MANIFEST_FILE);
125
+ let parsed;
126
+ try {
127
+ const manifestRealPath = assertRealPathInside(rootRealPath, manifestPath, SKILL_MANIFEST_FILE);
128
+ parsed = JSON.parse(readFileSync(manifestRealPath, "utf8"));
129
+ } catch (error) {
130
+ throw new Error(`cannot read Odai skill manifest ${manifestPath}: ${error instanceof Error ? error.message : String(error)}`);
131
+ }
132
+ const manifest = assertPlainObject(parsed, `Odai skill manifest ${manifestPath}`);
133
+ const unknown = Object.keys(manifest).filter((field) => !MANIFEST_FIELDS.has(field));
134
+ if (unknown.length > 0) {
135
+ throw new TypeError(`Odai skill manifest ${manifestPath} has unknown fields: ${unknown.join(", ")}`);
136
+ }
137
+ if (manifest.schemaVersion !== 1) {
138
+ throw new TypeError(`Odai skill manifest ${manifestPath} has unsupported schemaVersion ${String(manifest.schemaVersion)}`);
139
+ }
140
+ if (manifest.name !== "odai") throw new TypeError(`Odai skill manifest ${manifestPath} must name odai`);
141
+ const versionParts = parseSkillVersion(manifest.skillVersion, `Odai skill manifest ${manifestPath}.skillVersion`);
142
+ if (!Number.isSafeInteger(manifest.runtimeContract) || manifest.runtimeContract <= 0) {
143
+ throw new TypeError(`Odai skill manifest ${manifestPath}.runtimeContract must be a positive integer`);
144
+ }
145
+ if (!Array.isArray(manifest.requiredFiles) || manifest.requiredFiles.length === 0) {
146
+ throw new TypeError(`Odai skill manifest ${manifestPath}.requiredFiles must be a non-empty array`);
147
+ }
148
+ const requiredFiles = manifest.requiredFiles.map((file) => {
149
+ resolveBundleFile(skillRoot, file);
150
+ return file;
151
+ });
152
+ if (new Set(requiredFiles).size !== requiredFiles.length) {
153
+ throw new TypeError(`Odai skill manifest ${manifestPath}.requiredFiles contains duplicates`);
154
+ }
155
+ for (const required of REQUIRED_RUNTIME_FILES) {
156
+ if (!requiredFiles.includes(required)) {
157
+ throw new TypeError(`Odai skill manifest ${manifestPath} is missing required runtime file ${required}`);
158
+ }
159
+ }
160
+ return Object.freeze({
161
+ schemaVersion: 1,
162
+ name: "odai",
163
+ skillVersion: manifest.skillVersion,
164
+ versionParts,
165
+ runtimeContract: manifest.runtimeContract,
166
+ requiredFiles: Object.freeze(requiredFiles),
167
+ });
168
+ }
169
+
170
+ export function loadSkillBundle(skillPath, options = {}) {
171
+ const entryPath = resolve(skillPath);
172
+ const root = dirname(entryPath);
173
+ const rootRealPath = realpathSync(root);
174
+ const manifest = readSkillManifest(root);
175
+ const digest = createHash("sha256");
176
+ const contents = new Map();
177
+ digest.update(JSON.stringify({
178
+ schemaVersion: manifest.schemaVersion,
179
+ name: manifest.name,
180
+ skillVersion: manifest.skillVersion,
181
+ runtimeContract: manifest.runtimeContract,
182
+ requiredFiles: manifest.requiredFiles,
183
+ }));
184
+
185
+ for (const relativePath of [...manifest.requiredFiles].sort()) {
186
+ const path = resolveBundleFile(root, relativePath);
187
+ if (!existsSync(path)) throw new Error(`Odai skill bundle ${root} is missing ${relativePath}`);
188
+ const realPath = assertRealPathInside(rootRealPath, path, relativePath);
189
+ const content = readFileSync(realPath);
190
+ contents.set(relativePath, content);
191
+ digest.update("\0");
192
+ digest.update(relativePath);
193
+ digest.update("\0");
194
+ digest.update(content);
195
+ }
196
+
197
+ const skillText = contents.get("SKILL.md").toString("utf8").trim();
198
+ if (!skillText) throw new Error(`Odai canonical skill is empty: ${entryPath}`);
199
+ const frontmatter = skillText.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/u)?.[1];
200
+ if (!frontmatter || !/^name:\s*odai\s*$/mu.test(frontmatter)) {
201
+ throw new Error(`Odai canonical skill entry does not declare name odai: ${entryPath}`);
202
+ }
203
+
204
+ const roleContracts = Object.freeze(Object.fromEntries(["controller", "researcher", "planner", "executor", "reviewer", "frontend"].map((role) => {
205
+ const relativePath = `assets/routing-roles/${role}.md`;
206
+ const text = contents.get(relativePath).toString("utf8").trim();
207
+ if (!text) throw new Error(`Odai canonical ${role} role is unavailable: ${resolve(root, relativePath)}`);
208
+ return [role, text];
209
+ })));
210
+ const referenceContracts = Object.freeze(Object.fromEntries(["craft"].map((reference) => {
211
+ const relativePath = `references/${reference}.md`;
212
+ const text = contents.get(relativePath)?.toString("utf8").trim();
213
+ if (!text) throw new Error(`Odai canonical ${reference} reference is unavailable: ${resolve(root, relativePath)}`);
214
+ return [reference, text];
215
+ })));
216
+ const fileContents = Object.freeze(Object.fromEntries(
217
+ [...contents].map(([relativePath, content]) => [relativePath, content.toString("base64")]),
218
+ ));
219
+
220
+ const bundle = {
221
+ path: entryPath,
222
+ root,
223
+ source: typeof options.source === "string" && options.source ? options.source : "bundled",
224
+ provider: typeof options.provider === "string" && options.provider ? options.provider : "odai-dsh-runtime",
225
+ manifest,
226
+ skillText,
227
+ roleContracts,
228
+ referenceContracts,
229
+ digest: digest.digest("hex"),
230
+ };
231
+ Object.defineProperty(bundle, "fileContents", { value: fileContents, enumerable: false });
232
+ return Object.freeze(bundle);
233
+ }
234
+
235
+ export function readSkillBundleFile(bundle, relativePath) {
236
+ if (!bundle || typeof bundle !== "object" || !bundle.manifest?.requiredFiles?.includes(relativePath)) {
237
+ throw new TypeError(`unknown Odai skill bundle file: ${String(relativePath)}`);
238
+ }
239
+ const encoded = bundle.fileContents?.[relativePath];
240
+ if (typeof encoded !== "string") throw new Error(`Odai skill bundle snapshot is missing ${relativePath}`);
241
+ return Buffer.from(encoded, "base64");
242
+ }
243
+
244
+ function fallbackSelection(mode, bundled, reasonCode, detail, candidate) {
245
+ return Object.freeze({
246
+ mode,
247
+ status: "fallback",
248
+ reasonCode,
249
+ detail,
250
+ bundle: bundled,
251
+ ...(candidate ? { candidate } : {}),
252
+ });
253
+ }
254
+
255
+ function selected(mode, bundle, reasonCode, candidate) {
256
+ return Object.freeze({
257
+ mode,
258
+ status: "selected",
259
+ reasonCode,
260
+ bundle,
261
+ ...(candidate ? { candidate } : {}),
262
+ });
263
+ }
264
+
265
+ export function chooseSkillBundle({ mode, bundled, candidate, candidateError } = {}) {
266
+ if (!SKILL_SOURCE_MODES.includes(mode)) throw new TypeError(`unknown Odai skill source mode: ${String(mode)}`);
267
+ if (!bundled) throw new TypeError("bundled Odai skill is required");
268
+ if (mode === "bundled") return selected(mode, bundled, "bundled-configured");
269
+ if (candidateError) {
270
+ return fallbackSelection(mode, bundled, "external-invalid", candidateError instanceof Error ? candidateError.message : String(candidateError));
271
+ }
272
+ if (!candidate) {
273
+ return mode === "auto"
274
+ ? selected(mode, bundled, "external-not-installed")
275
+ : fallbackSelection(mode, bundled, "user-source-missing", "no compatible user-level Odai skill is installed");
276
+ }
277
+ if (candidate.manifest.runtimeContract !== ODAI_RUNTIME_CONTRACT) {
278
+ return fallbackSelection(
279
+ mode,
280
+ bundled,
281
+ "runtime-contract-mismatch",
282
+ `candidate runtimeContract ${candidate.manifest.runtimeContract} is incompatible with runtime contract ${ODAI_RUNTIME_CONTRACT}`,
283
+ candidate,
284
+ );
285
+ }
286
+
287
+ const versionOrder = compareSkillVersions(candidate.manifest.versionParts, bundled.manifest.versionParts);
288
+ if (versionOrder === 0) {
289
+ if (candidate.digest === bundled.digest) return selected(mode, bundled, "external-equivalent", candidate);
290
+ return fallbackSelection(
291
+ mode,
292
+ bundled,
293
+ "same-version-content-conflict",
294
+ `candidate ${candidate.manifest.skillVersion} differs from the bundled content with the same version`,
295
+ candidate,
296
+ );
297
+ }
298
+
299
+ if (mode === "user") {
300
+ if (!USER_SOURCES.has(candidate.source) && candidate.source !== "custom") {
301
+ return fallbackSelection(mode, bundled, "user-source-invalid", `source ${candidate.source} is not user-level`, candidate);
302
+ }
303
+ return selected(mode, candidate, "user-configured", candidate);
304
+ }
305
+ if (PROJECT_SOURCES.has(candidate.source)) return selected(mode, candidate, "project-scope-override", candidate);
306
+ if (USER_SOURCES.has(candidate.source)) {
307
+ return versionOrder > 0
308
+ ? selected(mode, candidate, "newer-user-skill", candidate)
309
+ : fallbackSelection(
310
+ mode,
311
+ bundled,
312
+ "user-skill-older",
313
+ `candidate ${candidate.manifest.skillVersion} is older than bundled ${bundled.manifest.skillVersion}`,
314
+ candidate,
315
+ );
316
+ }
317
+ return fallbackSelection(mode, bundled, "external-source-unsupported", `source ${candidate.source} cannot provide Odai governance`, candidate);
318
+ }