@salesforce/sfdx-agent-harness-openai 0.0.1 → 0.2.0

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.
@@ -0,0 +1,76 @@
1
+ import { type LogBus } from '@salesforce/agentic-common';
2
+ /**
3
+ * One discovered skill: the absolute path to its `SKILL.md` and the description
4
+ * string the loader surfaces to the model in the `load_skill` tool catalog. The
5
+ * frontmatter `name` (or the skill folder basename, if the frontmatter is
6
+ * missing) is the key in the {@link SkillMap}; this struct holds the rest.
7
+ */
8
+ export type SkillEntry = {
9
+ /** Absolute path to the resolved `SKILL.md` file. */
10
+ path: string;
11
+ /**
12
+ * Skill description from the YAML frontmatter `description:` field, trimmed.
13
+ * Empty string when the frontmatter omits `description:`. Surfaced verbatim
14
+ * in the `load_skill` tool description so the model can pick the right skill
15
+ * without reading the body.
16
+ */
17
+ description: string;
18
+ };
19
+ /**
20
+ * Discovered skill catalog: skill name → entry. The name is what the model
21
+ * passes back when it invokes `load_skill({ skill })`.
22
+ */
23
+ export type SkillMap = Map<string, SkillEntry>;
24
+ /**
25
+ * Resolve `config.skills` paths into a `Map<skillName, SkillEntry>`. Each entry
26
+ * in `skillPaths` may be:
27
+ *
28
+ * - A `SKILL.md` file path → one skill (named from frontmatter or parent folder
29
+ * basename).
30
+ * - A folder containing `SKILL.md` → one skill.
31
+ * - A parent folder containing one or more skill subfolders, each with its own
32
+ * `SKILL.md` → many skills. Subfolders without a `SKILL.md` are silently
33
+ * skipped so consumers can drop arbitrary subdirectories into their skill
34
+ * roots without breaking the loader.
35
+ *
36
+ * Each entry is resolved against `projectRoot` (via `path.resolve`) before it is
37
+ * `stat`ed, per the SDK `AgentConfig.skills` contract — relative entries resolve
38
+ * against the project root, absolute entries pass through unchanged. The
39
+ * resulting `SkillEntry.path` is therefore always absolute, which the
40
+ * `read_skill_file` sibling reader and the `<skill-location>` anchor both rely
41
+ * on.
42
+ *
43
+ * Mirrors `composeRulesAppend`'s contract for path validation: a configured
44
+ * entry that does not exist or that the harness cannot `stat` rejects
45
+ * `createAgent`, matching the rules-loader behavior so a typo in `config.skills`
46
+ * fails loud at registration time rather than silently dropping the skill at
47
+ * every turn.
48
+ *
49
+ * Skill name resolution: the frontmatter `name:` field wins; if it is missing or
50
+ * empty the parent-folder basename is used. When two configured paths resolve to
51
+ * the same name, **first writer wins** — the second entry is dropped.
52
+ *
53
+ * Frontmatter `description:` is captured verbatim (trimmed) and stored for the
54
+ * `load_skill` tool to surface in its description. When absent, the entry's
55
+ * `description` is the empty string.
56
+ *
57
+ * Frontmatter parsing is intentionally regex-based, not full YAML —
58
+ * `splitFrontmatterAndBody` from `@salesforce/agentic-common` already keeps the
59
+ * parser dependency-free, and the only fields this loader needs are `name` and
60
+ * `description`.
61
+ *
62
+ * A deliberate copy of the Claude harness's `skill-loader.ts`: a cross-harness
63
+ * import is lint-blocked by `harnessIsolationPack`, so the behavior is
64
+ * duplicated on purpose and drift-guarded by the shared
65
+ * `@salesforce/harness-conformance` `SKILL_LOADER_FIXTURES` table (the same
66
+ * posture as `rule-composer.ts` / `mcp-error-classifier.ts`).
67
+ */
68
+ export declare function buildSkillMap(skillPaths: readonly string[] | undefined, projectRoot: string, logBus?: LogBus): Promise<SkillMap>;
69
+ /**
70
+ * The unique set of skill directories in a {@link SkillMap} — each is the parent
71
+ * directory of a resolved `SKILL.md` (deduplicated, stable order). The
72
+ * `read_skill_file` tool scopes each read to one of these directories so a
73
+ * skill's relative reference (`references/...`, `assets/...`) resolves to a
74
+ * file inside its own skill folder and nothing else on disk is reachable.
75
+ */
76
+ export declare function skillDirectories(skillMap: SkillMap): string[];
@@ -0,0 +1,180 @@
1
+ /*
2
+ * Copyright 2026, Salesforce, Inc. All rights reserved.
3
+ * See LICENSE.txt for license terms.
4
+ */
5
+ import * as fs from 'node:fs/promises';
6
+ import * as path from 'node:path';
7
+ import { getErrorMessage, splitFrontmatterAndBody } from '@salesforce/agentic-common';
8
+ const SKILL_FILE_NAME = 'SKILL.md';
9
+ const FRONTMATTER_NAME_RE = /^name:\s*(.+?)\s*$/m;
10
+ const FRONTMATTER_DESCRIPTION_RE = /^description:\s*(.+?)\s*$/m;
11
+ /**
12
+ * Resolve `config.skills` paths into a `Map<skillName, SkillEntry>`. Each entry
13
+ * in `skillPaths` may be:
14
+ *
15
+ * - A `SKILL.md` file path → one skill (named from frontmatter or parent folder
16
+ * basename).
17
+ * - A folder containing `SKILL.md` → one skill.
18
+ * - A parent folder containing one or more skill subfolders, each with its own
19
+ * `SKILL.md` → many skills. Subfolders without a `SKILL.md` are silently
20
+ * skipped so consumers can drop arbitrary subdirectories into their skill
21
+ * roots without breaking the loader.
22
+ *
23
+ * Each entry is resolved against `projectRoot` (via `path.resolve`) before it is
24
+ * `stat`ed, per the SDK `AgentConfig.skills` contract — relative entries resolve
25
+ * against the project root, absolute entries pass through unchanged. The
26
+ * resulting `SkillEntry.path` is therefore always absolute, which the
27
+ * `read_skill_file` sibling reader and the `<skill-location>` anchor both rely
28
+ * on.
29
+ *
30
+ * Mirrors `composeRulesAppend`'s contract for path validation: a configured
31
+ * entry that does not exist or that the harness cannot `stat` rejects
32
+ * `createAgent`, matching the rules-loader behavior so a typo in `config.skills`
33
+ * fails loud at registration time rather than silently dropping the skill at
34
+ * every turn.
35
+ *
36
+ * Skill name resolution: the frontmatter `name:` field wins; if it is missing or
37
+ * empty the parent-folder basename is used. When two configured paths resolve to
38
+ * the same name, **first writer wins** — the second entry is dropped.
39
+ *
40
+ * Frontmatter `description:` is captured verbatim (trimmed) and stored for the
41
+ * `load_skill` tool to surface in its description. When absent, the entry's
42
+ * `description` is the empty string.
43
+ *
44
+ * Frontmatter parsing is intentionally regex-based, not full YAML —
45
+ * `splitFrontmatterAndBody` from `@salesforce/agentic-common` already keeps the
46
+ * parser dependency-free, and the only fields this loader needs are `name` and
47
+ * `description`.
48
+ *
49
+ * A deliberate copy of the Claude harness's `skill-loader.ts`: a cross-harness
50
+ * import is lint-blocked by `harnessIsolationPack`, so the behavior is
51
+ * duplicated on purpose and drift-guarded by the shared
52
+ * `@salesforce/harness-conformance` `SKILL_LOADER_FIXTURES` table (the same
53
+ * posture as `rule-composer.ts` / `mcp-error-classifier.ts`).
54
+ */
55
+ export async function buildSkillMap(skillPaths, projectRoot, logBus) {
56
+ const map = new Map();
57
+ if (!skillPaths || skillPaths.length === 0)
58
+ return map;
59
+ for (const entry of skillPaths) {
60
+ // Resolve relative entries against the project root per the SDK
61
+ // `AgentConfig.skills` contract; an absolute entry is returned unchanged.
62
+ // This is what makes `SkillEntry.path` (and therefore the sibling-file
63
+ // reader + the `<skill-location>` anchor) absolute, as their contracts
64
+ // require — without it a relative entry would resolve against the host
65
+ // process cwd, which is not the project root.
66
+ const resolved = path.resolve(projectRoot, entry);
67
+ let stats;
68
+ try {
69
+ stats = await fs.stat(resolved);
70
+ }
71
+ catch (err) {
72
+ throw new Error(`Failed to load skills from "${entry}": ${getErrorMessage(err)}`, { cause: err });
73
+ }
74
+ if (stats.isFile()) {
75
+ if (path.basename(resolved) !== SKILL_FILE_NAME)
76
+ continue;
77
+ await ingestSkillFile(resolved, map, logBus);
78
+ continue;
79
+ }
80
+ if (!stats.isDirectory())
81
+ continue;
82
+ // Single-skill folder containing SKILL.md?
83
+ const candidate = path.join(resolved, SKILL_FILE_NAME);
84
+ if (await isFile(candidate)) {
85
+ await ingestSkillFile(candidate, map, logBus);
86
+ continue;
87
+ }
88
+ // Parent folder: enumerate immediate subdirectories.
89
+ let children;
90
+ try {
91
+ children = await fs.readdir(resolved, { withFileTypes: true });
92
+ }
93
+ catch (err) {
94
+ throw new Error(`Failed to load skills from "${entry}": ${getErrorMessage(err)}`, { cause: err });
95
+ }
96
+ for (const child of children) {
97
+ if (!child.isDirectory())
98
+ continue;
99
+ const childSkillMd = path.join(resolved, child.name, SKILL_FILE_NAME);
100
+ if (!(await isFile(childSkillMd)))
101
+ continue;
102
+ await ingestSkillFile(childSkillMd, map, logBus);
103
+ }
104
+ }
105
+ return map;
106
+ }
107
+ /**
108
+ * The unique set of skill directories in a {@link SkillMap} — each is the parent
109
+ * directory of a resolved `SKILL.md` (deduplicated, stable order). The
110
+ * `read_skill_file` tool scopes each read to one of these directories so a
111
+ * skill's relative reference (`references/...`, `assets/...`) resolves to a
112
+ * file inside its own skill folder and nothing else on disk is reachable.
113
+ */
114
+ export function skillDirectories(skillMap) {
115
+ const dirs = new Set();
116
+ for (const entry of skillMap.values()) {
117
+ dirs.add(path.dirname(entry.path));
118
+ }
119
+ return [...dirs];
120
+ }
121
+ async function ingestSkillFile(skillMdPath, map, logBus) {
122
+ let raw;
123
+ try {
124
+ raw = await fs.readFile(skillMdPath, 'utf8');
125
+ }
126
+ catch (err) {
127
+ throw new Error(`Failed to read skill file "${skillMdPath}": ${getErrorMessage(err)}`, { cause: err });
128
+ }
129
+ const { frontmatter } = splitFrontmatterAndBody(raw);
130
+ const fm = frontmatter ?? '';
131
+ const name = readField(fm, FRONTMATTER_NAME_RE) ?? path.basename(path.dirname(skillMdPath));
132
+ const description = readField(fm, FRONTMATTER_DESCRIPTION_RE) ?? '';
133
+ if (!name)
134
+ return;
135
+ if (map.has(name)) {
136
+ // First-writer-wins by design (consumers can layer skill folders), but
137
+ // warn so the silent shadow doesn't burn debugging time.
138
+ const winner = map.get(name);
139
+ logBus?.warn(`Skill name "${name}" already loaded; ignoring duplicate at "${skillMdPath}".`, {
140
+ skillName: name,
141
+ keptPath: winner.path,
142
+ ignoredPath: skillMdPath,
143
+ });
144
+ return;
145
+ }
146
+ map.set(name, { path: skillMdPath, description });
147
+ }
148
+ function readField(frontmatter, re) {
149
+ const match = re.exec(frontmatter);
150
+ if (!match?.[1])
151
+ return undefined;
152
+ const trimmed = match[1].trim();
153
+ if (trimmed.length === 0)
154
+ return undefined;
155
+ // Strip a single matched pair of YAML scalar quotes (`"..."` or `'...'`).
156
+ // YAML allows quoting frontmatter scalars to escape colons, leading
157
+ // whitespace, etc. A regex-based reader (kept for the no-YAML-runtime
158
+ // stance) would otherwise capture the quotes verbatim — the resulting
159
+ // `name:` key would include them and the model's `load_skill({ skill })`
160
+ // call would miss the catalog entry. Keeps the cross-harness contract on the
161
+ // same observable shape as Mastra's full YAML parser.
162
+ if (trimmed.length >= 2) {
163
+ const first = trimmed.charAt(0);
164
+ const last = trimmed.charAt(trimmed.length - 1);
165
+ if ((first === '"' || first === "'") && first === last) {
166
+ return trimmed.slice(1, -1);
167
+ }
168
+ }
169
+ return trimmed;
170
+ }
171
+ async function isFile(p) {
172
+ try {
173
+ const s = await fs.stat(p);
174
+ return s.isFile();
175
+ }
176
+ catch {
177
+ return false;
178
+ }
179
+ }
180
+ //# sourceMappingURL=skill-loader.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"skill-loader.js","sourceRoot":"","sources":["../src/skill-loader.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,MAAM,kBAAkB,CAAC;AACvC,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAClC,OAAO,EAAE,eAAe,EAAe,uBAAuB,EAAE,MAAM,4BAA4B,CAAC;AA0BnG,MAAM,eAAe,GAAG,UAAU,CAAC;AACnC,MAAM,mBAAmB,GAAG,qBAAqB,CAAC;AAClD,MAAM,0BAA0B,GAAG,4BAA4B,CAAC;AAEhE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CAC/B,UAAyC,EACzC,WAAmB,EACnB,MAAe;IAEf,MAAM,GAAG,GAAa,IAAI,GAAG,EAAE,CAAC;IAChC,IAAI,CAAC,UAAU,IAAI,UAAU,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,GAAG,CAAC;IAEvD,KAAK,MAAM,KAAK,IAAI,UAAU,EAAE,CAAC;QAC7B,gEAAgE;QAChE,0EAA0E;QAC1E,uEAAuE;QACvE,uEAAuE;QACvE,uEAAuE;QACvE,8CAA8C;QAC9C,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,CAAC,WAAW,EAAE,KAAK,CAAC,CAAC;QAClD,IAAI,KAA0C,CAAC;QAC/C,IAAI,CAAC;YACD,KAAK,GAAG,MAAM,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QACpC,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACX,MAAM,IAAI,KAAK,CAAC,+BAA+B,KAAK,MAAM,eAAe,CAAC,GAAG,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,CAAC;QACtG,CAAC;QAED,IAAI,KAAK,CAAC,MAAM,EAAE,EAAE,CAAC;YACjB,IAAI,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,KAAK,eAAe;gBAAE,SAAS;YAC1D,MAAM,eAAe,CAAC,QAAQ,EAAE,GAAG,EAAE,MAAM,CAAC,CAAC;YAC7C,SAAS;QACb,CAAC;QACD,IAAI,CAAC,KAAK,CAAC,WAAW,EAAE;YAAE,SAAS;QAEnC,2CAA2C;QAC3C,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,eAAe,CAAC,CAAC;QACvD,IAAI,MAAM,MAAM,CAAC,SAAS,CAAC,EAAE,CAAC;YAC1B,MAAM,eAAe,CAAC,SAAS,EAAE,GAAG,EAAE,MAAM,CAAC,CAAC;YAC9C,SAAS;QACb,CAAC;QAED,qDAAqD;QACrD,IAAI,QAAoC,CAAC;QACzC,IAAI,CAAC;YACD,QAAQ,GAAG,MAAM,EAAE,CAAC,OAAO,CAAC,QAAQ,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAC;QACnE,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACX,MAAM,IAAI,KAAK,CAAC,+BAA+B,KAAK,MAAM,eAAe,CAAC,GAAG,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,CAAC;QACtG,CAAC;QACD,KAAK,MAAM,KAAK,IAAI,QAAQ,EAAE,CAAC;YAC3B,IAAI,CAAC,KAAK,CAAC,WAAW,EAAE;gBAAE,SAAS;YACnC,MAAM,YAAY,GAAG,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,KAAK,CAAC,IAAI,EAAE,eAAe,CAAC,CAAC;YACtE,IAAI,CAAC,CAAC,MAAM,MAAM,CAAC,YAAY,CAAC,CAAC;gBAAE,SAAS;YAC5C,MAAM,eAAe,CAAC,YAAY,EAAE,GAAG,EAAE,MAAM,CAAC,CAAC;QACrD,CAAC;IACL,CAAC;IAED,OAAO,GAAG,CAAC;AACf,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,gBAAgB,CAAC,QAAkB;IAC/C,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;IAC/B,KAAK,MAAM,KAAK,IAAI,QAAQ,CAAC,MAAM,EAAE,EAAE,CAAC;QACpC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC;IACvC,CAAC;IACD,OAAO,CAAC,GAAG,IAAI,CAAC,CAAC;AACrB,CAAC;AAED,KAAK,UAAU,eAAe,CAAC,WAAmB,EAAE,GAAa,EAAE,MAAe;IAC9E,IAAI,GAAW,CAAC;IAChB,IAAI,CAAC;QACD,GAAG,GAAG,MAAM,EAAE,CAAC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC,CAAC;IACjD,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACX,MAAM,IAAI,KAAK,CAAC,8BAA8B,WAAW,MAAM,eAAe,CAAC,GAAG,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,CAAC;IAC3G,CAAC;IACD,MAAM,EAAE,WAAW,EAAE,GAAG,uBAAuB,CAAC,GAAG,CAAC,CAAC;IACrD,MAAM,EAAE,GAAG,WAAW,IAAI,EAAE,CAAC;IAC7B,MAAM,IAAI,GAAG,SAAS,CAAC,EAAE,EAAE,mBAAmB,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC;IAC5F,MAAM,WAAW,GAAG,SAAS,CAAC,EAAE,EAAE,0BAA0B,CAAC,IAAI,EAAE,CAAC;IACpE,IAAI,CAAC,IAAI;QAAE,OAAO;IAClB,IAAI,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;QAChB,uEAAuE;QACvE,yDAAyD;QACzD,MAAM,MAAM,GAAG,GAAG,CAAC,GAAG,CAAC,IAAI,CAAE,CAAC;QAC9B,MAAM,EAAE,IAAI,CAAC,eAAe,IAAI,4CAA4C,WAAW,IAAI,EAAE;YACzF,SAAS,EAAE,IAAI;YACf,QAAQ,EAAE,MAAM,CAAC,IAAI;YACrB,WAAW,EAAE,WAAW;SAC3B,CAAC,CAAC;QACH,OAAO;IACX,CAAC;IACD,GAAG,CAAC,GAAG,CAAC,IAAI,EAAE,EAAE,IAAI,EAAE,WAAW,EAAE,WAAW,EAAE,CAAC,CAAC;AACtD,CAAC;AAED,SAAS,SAAS,CAAC,WAAmB,EAAE,EAAU;IAC9C,MAAM,KAAK,GAAG,EAAE,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;IACnC,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC;QAAE,OAAO,SAAS,CAAC;IAClC,MAAM,OAAO,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;IAChC,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,SAAS,CAAC;IAC3C,0EAA0E;IAC1E,oEAAoE;IACpE,sEAAsE;IACtE,sEAAsE;IACtE,yEAAyE;IACzE,6EAA6E;IAC7E,sDAAsD;IACtD,IAAI,OAAO,CAAC,MAAM,IAAI,CAAC,EAAE,CAAC;QACtB,MAAM,KAAK,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;QAChC,MAAM,IAAI,GAAG,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;QAChD,IAAI,CAAC,KAAK,KAAK,GAAG,IAAI,KAAK,KAAK,GAAG,CAAC,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;YACrD,OAAO,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;QAChC,CAAC;IACL,CAAC;IACD,OAAO,OAAO,CAAC;AACnB,CAAC;AAED,KAAK,UAAU,MAAM,CAAC,CAAS;IAC3B,IAAI,CAAC;QACD,MAAM,CAAC,GAAG,MAAM,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAC3B,OAAO,CAAC,CAAC,MAAM,EAAE,CAAC;IACtB,CAAC;IAAC,MAAM,CAAC;QACL,OAAO,KAAK,CAAC;IACjB,CAAC;AACL,CAAC"}