odai-dsh-agent 0.2.1 → 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 (51) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +138 -138
  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/governance.mjs +128 -128
  9. package/preset/odai/runtime/index.mjs +2461 -2263
  10. package/preset/odai/runtime/output-config.mjs +360 -304
  11. package/preset/odai/runtime/research-packet.mjs +150 -150
  12. package/preset/odai/runtime/responsibility-scope.mjs +35 -2
  13. package/preset/odai/runtime/role-overlays.mjs +27 -27
  14. package/preset/odai/runtime/route-card.mjs +168 -152
  15. package/preset/odai/runtime/router.mjs +683 -651
  16. package/preset/odai/runtime/routing-config.mjs +480 -436
  17. package/preset/odai/runtime/routing-context.mjs +321 -321
  18. package/preset/odai/runtime/semantic-memory-store.mjs +452 -452
  19. package/preset/odai/runtime/semantic-memory.mjs +842 -842
  20. package/preset/odai/runtime/session-compat.mjs +389 -389
  21. package/preset/odai/runtime/skill-bundle.mjs +318 -318
  22. package/preset/odai/runtime/skill-evolution.mjs +1413 -1413
  23. package/preset/odai/skills/odai/agents/openai.yaml +4 -4
  24. package/preset/odai/skills/odai/assets/claude-agents/agent.md +11 -11
  25. package/preset/odai/skills/odai/assets/codex-agents/config.toml +11 -11
  26. package/preset/odai/skills/odai/assets/codex-agents/role.toml +5 -5
  27. package/preset/odai/skills/odai/assets/copilot-agents/agent.md +13 -13
  28. package/preset/odai/skills/odai/assets/hooks-policy.example.json +24 -24
  29. package/preset/odai/skills/odai/assets/routing-roles/controller.md +15 -15
  30. package/preset/odai/skills/odai/assets/routing-roles/executor.md +11 -11
  31. package/preset/odai/skills/odai/assets/routing-roles/frontend.md +7 -7
  32. package/preset/odai/skills/odai/assets/routing-roles/planner.md +29 -29
  33. package/preset/odai/skills/odai/assets/routing-roles/researcher.md +6 -6
  34. package/preset/odai/skills/odai/assets/routing-roles/reviewer.md +11 -11
  35. package/preset/odai/skills/odai/assets/task-state.md +14 -14
  36. package/preset/odai/skills/odai/manifest.json +37 -37
  37. package/preset/odai/skills/odai/references/craft.md +54 -54
  38. package/preset/odai/skills/odai/references/dao.md +30 -30
  39. package/preset/odai/skills/odai/references/leverage.md +53 -53
  40. package/preset/odai/skills/odai/references/planning.md +82 -82
  41. package/preset/odai/skills/odai/references/support.md +36 -36
  42. package/preset/odai/skills/odai/references/verification.md +23 -23
  43. package/preset/odai/skills/odai/scripts/build-hooks.mjs +273 -273
  44. package/preset/odai/skills/odai/scripts/build-routing.mjs +274 -274
  45. package/preset/odai/skills/odai/scripts/install-routing.mjs +397 -397
  46. package/preset/odai/skills/odai/scripts/odai-hook.mjs +457 -457
  47. package/preset/odai/skills/odai/scripts/run-role.mjs +336 -336
  48. package/preset/odai/skills/odai/scripts/run-routing.mjs +1154 -1154
  49. package/preset/odai/skills/odai/scripts/verify-routing.mjs +103 -103
  50. package/scripts/verify-agent-load.mjs +257 -257
  51. 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
+ }