@agentworkforce/workload-router 0.19.0 → 2.1.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.
- package/CHANGELOG.md +24 -0
- package/README.md +2 -2
- package/dist/eval.d.ts +1 -1
- package/dist/eval.d.ts.map +1 -1
- package/dist/index.d.ts +1 -374
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -886
- package/dist/index.js.map +1 -1
- package/dist/index.test.js +3 -219
- package/dist/index.test.js.map +1 -1
- package/package.json +4 -1
package/dist/index.js
CHANGED
|
@@ -1,845 +1,6 @@
|
|
|
1
|
+
import { deepFreeze, isObject, isTier, materializeSkills, materializeSkillsFor, parsePersonaSpec, PERSONA_INTENTS, PERSONA_TIERS, resolveSidecar, sidecarSelectionFields, buildInstallArtifacts, buildCleanupArtifacts } from '@agentworkforce/persona-kit';
|
|
1
2
|
import { personaImprover, personaMaker } from './generated/personas.js';
|
|
2
3
|
import defaultRoutingProfileJson from '../routing-profiles/default.json' with { type: 'json' };
|
|
3
|
-
export const HARNESS_VALUES = ['opencode', 'codex', 'claude'];
|
|
4
|
-
export const PERSONA_TIERS = ['best', 'best-value', 'minimum'];
|
|
5
|
-
export const PERSONA_TAGS = [
|
|
6
|
-
'planning',
|
|
7
|
-
'implementation',
|
|
8
|
-
'review',
|
|
9
|
-
'testing',
|
|
10
|
-
'debugging',
|
|
11
|
-
'documentation',
|
|
12
|
-
'release',
|
|
13
|
-
'discovery',
|
|
14
|
-
'analytics'
|
|
15
|
-
];
|
|
16
|
-
export const PERSONA_INTENTS = [
|
|
17
|
-
'implement-frontend',
|
|
18
|
-
'review',
|
|
19
|
-
'architecture-plan',
|
|
20
|
-
'requirements-analysis',
|
|
21
|
-
'debugging',
|
|
22
|
-
'security-review',
|
|
23
|
-
'documentation',
|
|
24
|
-
'verification',
|
|
25
|
-
'test-strategy',
|
|
26
|
-
'tdd-enforcement',
|
|
27
|
-
'flake-investigation',
|
|
28
|
-
'opencode-workflow-correctness',
|
|
29
|
-
'npm-provenance',
|
|
30
|
-
'cloud-sandbox-infra',
|
|
31
|
-
'sage-slack-egress-migration',
|
|
32
|
-
'sage-proactive-rewire',
|
|
33
|
-
'cloud-slack-proxy-guard',
|
|
34
|
-
'sage-cloud-e2e-conduction',
|
|
35
|
-
'capability-discovery',
|
|
36
|
-
'npm-package-compat',
|
|
37
|
-
'posthog',
|
|
38
|
-
'persona-authoring',
|
|
39
|
-
'persona-improvement',
|
|
40
|
-
'agent-relay-workflow',
|
|
41
|
-
'slop-audit',
|
|
42
|
-
'api-contract-review',
|
|
43
|
-
'local-stack-orchestration',
|
|
44
|
-
'e2e-validation',
|
|
45
|
-
'write-integration-tests',
|
|
46
|
-
'relay-orchestrator'
|
|
47
|
-
];
|
|
48
|
-
export const BUILT_IN_PERSONA_INTENTS = ['persona-authoring', 'persona-improvement'];
|
|
49
|
-
export const CODEX_SANDBOX_MODES = [
|
|
50
|
-
'read-only',
|
|
51
|
-
'workspace-write',
|
|
52
|
-
'danger-full-access'
|
|
53
|
-
];
|
|
54
|
-
export const CODEX_APPROVAL_POLICIES = [
|
|
55
|
-
'untrusted',
|
|
56
|
-
'on-failure',
|
|
57
|
-
'on-request',
|
|
58
|
-
'never'
|
|
59
|
-
];
|
|
60
|
-
/**
|
|
61
|
-
* Sidecar markdown delivery mode. `overwrite` writes only the persona's
|
|
62
|
-
* resolved markdown into the harness's mount file. `extend` reads the
|
|
63
|
-
* caller's real-cwd file (if any) and prepends it to the persona content,
|
|
64
|
-
* separated by `\n\n---\n\n`. With no real-cwd file `extend` degrades to
|
|
65
|
-
* `overwrite` semantics — the persona content lands alone.
|
|
66
|
-
*/
|
|
67
|
-
export const SIDECAR_MD_MODES = ['overwrite', 'extend'];
|
|
68
|
-
export const PERMISSION_MODES = [
|
|
69
|
-
'default',
|
|
70
|
-
'acceptEdits',
|
|
71
|
-
'bypassPermissions',
|
|
72
|
-
'plan'
|
|
73
|
-
];
|
|
74
|
-
// ---------------------------------------------------------------------------
|
|
75
|
-
// Skill materialization
|
|
76
|
-
// ---------------------------------------------------------------------------
|
|
77
|
-
//
|
|
78
|
-
// Personas declare *what* skill they need via `skills: [{ id, source, ... }]`.
|
|
79
|
-
// The SDK is the only layer that knows *how* to make that skill available to
|
|
80
|
-
// a given harness — because each harness has its own on-disk convention and
|
|
81
|
-
// its own prpm install flag. Keeping this mapping here means:
|
|
82
|
-
//
|
|
83
|
-
// 1. Workflow authors never hand-type `prpm install ... --as codex`.
|
|
84
|
-
// 2. Changing install rules is a one-line SDK edit, not a repo-wide grep.
|
|
85
|
-
// 3. Persona JSON stays harness-agnostic and forward-compatible.
|
|
86
|
-
//
|
|
87
|
-
// `materializeSkills` is a pure function: it returns the install plan but
|
|
88
|
-
// never touches the filesystem or spawns processes. Callers (relay workflows,
|
|
89
|
-
// the OpenClaw spawner, ad-hoc scripts) decide how to execute it.
|
|
90
|
-
export const SKILL_SOURCE_KINDS = ['prpm', 'skill.sh'];
|
|
91
|
-
export const HARNESS_SKILL_TARGETS = {
|
|
92
|
-
claude: { asFlag: 'claude', dir: '.claude/skills' },
|
|
93
|
-
codex: { asFlag: 'codex', dir: '.agents/skills' },
|
|
94
|
-
opencode: { asFlag: 'opencode', dir: '.skills' }
|
|
95
|
-
};
|
|
96
|
-
const PRPM_URL_RE = /^https?:\/\/prpm\.dev\/packages\/([^/\s?#]+)\/([^/\s?#]+)\/?(?:[?#].*)?$/i;
|
|
97
|
-
const PRPM_BARE_REF_RE = /^([^/\s]+)\/([^/\s]+)$/;
|
|
98
|
-
function lastSegment(ref) {
|
|
99
|
-
const slash = ref.lastIndexOf('/');
|
|
100
|
-
return slash >= 0 ? ref.slice(slash + 1) : ref;
|
|
101
|
-
}
|
|
102
|
-
const prpmProvider = {
|
|
103
|
-
kind: 'prpm',
|
|
104
|
-
parse(source) {
|
|
105
|
-
const urlMatch = source.match(PRPM_URL_RE);
|
|
106
|
-
if (urlMatch) {
|
|
107
|
-
const ref = `${urlMatch[1]}/${urlMatch[2]}`;
|
|
108
|
-
return { kind: 'prpm', packageRef: ref, installedName: lastSegment(ref) };
|
|
109
|
-
}
|
|
110
|
-
const bareMatch = source.match(PRPM_BARE_REF_RE);
|
|
111
|
-
if (bareMatch) {
|
|
112
|
-
return { kind: 'prpm', packageRef: source, installedName: lastSegment(source) };
|
|
113
|
-
}
|
|
114
|
-
return null;
|
|
115
|
-
},
|
|
116
|
-
buildInstallCommand(ref, harness) {
|
|
117
|
-
const target = HARNESS_SKILL_TARGETS[harness];
|
|
118
|
-
return Object.freeze([
|
|
119
|
-
'npx',
|
|
120
|
-
'-y',
|
|
121
|
-
'prpm',
|
|
122
|
-
'install',
|
|
123
|
-
ref.packageRef,
|
|
124
|
-
'--as',
|
|
125
|
-
target.asFlag
|
|
126
|
-
]);
|
|
127
|
-
},
|
|
128
|
-
cleanupPaths(ref, harness) {
|
|
129
|
-
const target = HARNESS_SKILL_TARGETS[harness];
|
|
130
|
-
return Object.freeze([`${target.dir}/${ref.installedName}`]);
|
|
131
|
-
}
|
|
132
|
-
};
|
|
133
|
-
// skill.sh source forms:
|
|
134
|
-
// - `<github-url>#<skill-name>`
|
|
135
|
-
// - `<github-url>/tree/<ref>/<path-to-skill>`
|
|
136
|
-
// Examples:
|
|
137
|
-
// - `https://github.com/vercel-labs/skills#find-skills`
|
|
138
|
-
// - `https://github.com/wsimmonds/claude-nextjs-skills/tree/main/nextjs-anti-patterns`
|
|
139
|
-
const SKILL_SH_URL_RE = /^(https?:\/\/github\.com\/[^/\s?#]+\/[^/\s?#]+?)(?:\.git)?#([^\s?#]+)$/i;
|
|
140
|
-
const SKILL_SH_TREE_URL_RE = /^(https?:\/\/github\.com\/[^/\s?#]+\/[^/\s?#]+?)(?:\.git)?\/tree\/([^/\s?#]+)\/([^?#]+?)(?:[?#].*)?$/i;
|
|
141
|
-
const SKILL_NAME_RE = /^(?!\.{1,2}$)[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
142
|
-
function toSafeSkillName(raw) {
|
|
143
|
-
const name = raw.trim();
|
|
144
|
-
return SKILL_NAME_RE.test(name) ? name : null;
|
|
145
|
-
}
|
|
146
|
-
/**
|
|
147
|
-
* Paths `npx skills add` writes per install. Mirrors the on-disk layout from
|
|
148
|
-
* a live `npx -y skills add ... -y` run (universal dir + harness-side
|
|
149
|
-
* symlinks). `skills-lock.json` is deliberately excluded so repeat runs can
|
|
150
|
-
* re-resolve from the lock instead of refetching sources.
|
|
151
|
-
*/
|
|
152
|
-
function skillShArtifactPaths(installedName) {
|
|
153
|
-
return Object.freeze([
|
|
154
|
-
`.agents/skills/${installedName}`,
|
|
155
|
-
`.claude/skills/${installedName}`,
|
|
156
|
-
`.factory/skills/${installedName}`,
|
|
157
|
-
`.kiro/skills/${installedName}`,
|
|
158
|
-
`skills/${installedName}`
|
|
159
|
-
]);
|
|
160
|
-
}
|
|
161
|
-
const skillShProvider = {
|
|
162
|
-
kind: 'skill.sh',
|
|
163
|
-
parse(source) {
|
|
164
|
-
const match = source.match(SKILL_SH_URL_RE);
|
|
165
|
-
if (match) {
|
|
166
|
-
const [, repoUrl, rawSkillName] = match;
|
|
167
|
-
const skillName = toSafeSkillName(rawSkillName);
|
|
168
|
-
if (!skillName)
|
|
169
|
-
return null;
|
|
170
|
-
return {
|
|
171
|
-
kind: 'skill.sh',
|
|
172
|
-
// packageRef preserves the full `<repo>#<skill>` shape so the command builder
|
|
173
|
-
// can reconstruct both halves without re-parsing the original source.
|
|
174
|
-
packageRef: `${repoUrl}#${skillName}`,
|
|
175
|
-
installedName: skillName
|
|
176
|
-
};
|
|
177
|
-
}
|
|
178
|
-
const treeMatch = source.match(SKILL_SH_TREE_URL_RE);
|
|
179
|
-
if (!treeMatch)
|
|
180
|
-
return null;
|
|
181
|
-
const [, repoUrl, ref, skillPath] = treeMatch;
|
|
182
|
-
const skillName = toSafeSkillName(skillPath.split('/').filter(Boolean).at(-1) ?? '');
|
|
183
|
-
if (!skillName)
|
|
184
|
-
return null;
|
|
185
|
-
return {
|
|
186
|
-
kind: 'skill.sh',
|
|
187
|
-
packageRef: `${repoUrl}/tree/${ref}#${skillName}`,
|
|
188
|
-
installedName: skillName
|
|
189
|
-
};
|
|
190
|
-
},
|
|
191
|
-
buildInstallCommand(ref) {
|
|
192
|
-
const [repoUrl, skillName] = ref.packageRef.split('#');
|
|
193
|
-
return Object.freeze([
|
|
194
|
-
'npx',
|
|
195
|
-
'-y',
|
|
196
|
-
'skills',
|
|
197
|
-
'add',
|
|
198
|
-
repoUrl,
|
|
199
|
-
'--skill',
|
|
200
|
-
skillName,
|
|
201
|
-
'-y'
|
|
202
|
-
]);
|
|
203
|
-
},
|
|
204
|
-
cleanupPaths(ref) {
|
|
205
|
-
// skill.sh installs the same universal dir + harness symlinks regardless
|
|
206
|
-
// of which agent is the "host" — clean all of them so nothing leaks into
|
|
207
|
-
// `.claude/`, `.agents/`, etc. after the persona run.
|
|
208
|
-
return skillShArtifactPaths(ref.installedName);
|
|
209
|
-
}
|
|
210
|
-
};
|
|
211
|
-
const SKILL_PROVIDERS = Object.freeze([prpmProvider, skillShProvider]);
|
|
212
|
-
function resolveSkillSource(source) {
|
|
213
|
-
for (const provider of SKILL_PROVIDERS) {
|
|
214
|
-
const parsed = provider.parse(source);
|
|
215
|
-
if (parsed) {
|
|
216
|
-
return parsed;
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
throw new Error(`Unsupported skill source: ${source}. ` +
|
|
220
|
-
`Supported forms: prpm.dev package URL (https://prpm.dev/packages/<scope>/<name>), ` +
|
|
221
|
-
`bare "<scope>/<name>" prpm reference, ` +
|
|
222
|
-
`skill.sh github URL with skill fragment (https://github.com/<org>/<repo>#<skill>), ` +
|
|
223
|
-
`or GitHub tree URL to a skill directory (https://github.com/<org>/<repo>/tree/<ref>/<skill>).`);
|
|
224
|
-
}
|
|
225
|
-
function providerFor(kind) {
|
|
226
|
-
const provider = SKILL_PROVIDERS.find((p) => p.kind === kind);
|
|
227
|
-
if (!provider) {
|
|
228
|
-
throw new Error(`No skill provider registered for kind: ${kind}`);
|
|
229
|
-
}
|
|
230
|
-
return provider;
|
|
231
|
-
}
|
|
232
|
-
/**
|
|
233
|
-
* Given a set of persona skills and the harness the persona will run under,
|
|
234
|
-
* produce the concrete install plan: which install invocations to run, where
|
|
235
|
-
* the skill will land on disk, and which artifact paths should be cleaned up
|
|
236
|
-
* after the persona run to keep the workspace tidy.
|
|
237
|
-
*
|
|
238
|
-
* Pure function — does not execute commands or touch the filesystem.
|
|
239
|
-
*/
|
|
240
|
-
export function materializeSkills(skills, harness, options = {}) {
|
|
241
|
-
const target = HARNESS_SKILL_TARGETS[harness];
|
|
242
|
-
if (!target) {
|
|
243
|
-
throw new Error(`No skill install target configured for harness: ${harness}`);
|
|
244
|
-
}
|
|
245
|
-
const { installRoot } = options;
|
|
246
|
-
if (installRoot !== undefined && harness !== 'claude') {
|
|
247
|
-
throw new Error(`installRoot is only supported for the claude harness (got: ${harness}). ` +
|
|
248
|
-
`codex and opencode still install into the harness's conventional repo-relative directory.`);
|
|
249
|
-
}
|
|
250
|
-
const installs = skills.map((skill) => {
|
|
251
|
-
const resolved = resolveSkillSource(skill.source);
|
|
252
|
-
const provider = providerFor(resolved.kind);
|
|
253
|
-
const baseCommand = provider.buildInstallCommand(resolved, harness);
|
|
254
|
-
// In session-install-root mode, the install runs `cd <installRoot> && <prpm>`
|
|
255
|
-
// so that prpm's harness-relative dirs (`.claude/skills/<name>`) land
|
|
256
|
-
// inside the stage dir instead of the user's repo. The per-install command
|
|
257
|
-
// stays self-contained so callers who run a single install.installCommand
|
|
258
|
-
// directly still get the correct placement.
|
|
259
|
-
const installCommand = installRoot !== undefined
|
|
260
|
-
? Object.freeze([
|
|
261
|
-
'sh',
|
|
262
|
-
'-c',
|
|
263
|
-
`cd ${shellEscape(installRoot)} && ${commandToShellString(baseCommand)}`
|
|
264
|
-
])
|
|
265
|
-
: baseCommand;
|
|
266
|
-
// For prompt-injection fallback we still want a single canonical manifest
|
|
267
|
-
// path. prpm installs into the harness target dir; skill.sh installs into
|
|
268
|
-
// its universal `.agents/skills` dir regardless of harness, so key off
|
|
269
|
-
// whichever cleanup path ends in the installed name.
|
|
270
|
-
const repoRelativeDir = resolved.kind === 'skill.sh'
|
|
271
|
-
? `.agents/skills/${resolved.installedName}`
|
|
272
|
-
: `${target.dir}/${resolved.installedName}`;
|
|
273
|
-
const installedDir = installRoot !== undefined ? `${installRoot}/${repoRelativeDir}` : repoRelativeDir;
|
|
274
|
-
// When the plan stages into `installRoot`, cleanup targets the whole
|
|
275
|
-
// session dir (handled at plan level in buildCleanupArtifacts). Leave
|
|
276
|
-
// per-skill cleanupPaths empty so callers running individual
|
|
277
|
-
// install.cleanupPaths don't accidentally remove unrelated things.
|
|
278
|
-
const cleanupPaths = installRoot !== undefined
|
|
279
|
-
? Object.freeze([])
|
|
280
|
-
: provider.cleanupPaths(resolved, harness);
|
|
281
|
-
return {
|
|
282
|
-
skillId: skill.id,
|
|
283
|
-
source: skill.source,
|
|
284
|
-
sourceKind: resolved.kind,
|
|
285
|
-
packageRef: resolved.packageRef,
|
|
286
|
-
harness,
|
|
287
|
-
installCommand,
|
|
288
|
-
installedDir,
|
|
289
|
-
installedManifest: `${installedDir}/SKILL.md`,
|
|
290
|
-
cleanupPaths
|
|
291
|
-
};
|
|
292
|
-
});
|
|
293
|
-
return {
|
|
294
|
-
harness,
|
|
295
|
-
installs,
|
|
296
|
-
...(installRoot !== undefined ? { sessionInstallRoot: installRoot } : {})
|
|
297
|
-
};
|
|
298
|
-
}
|
|
299
|
-
/**
|
|
300
|
-
* Convenience wrapper: derive the install plan directly from a resolved
|
|
301
|
-
* persona selection, using its tier's harness automatically.
|
|
302
|
-
*/
|
|
303
|
-
export function materializeSkillsFor(selection, options = {}) {
|
|
304
|
-
return materializeSkills(selection.skills, selection.runtime.harness, options);
|
|
305
|
-
}
|
|
306
|
-
function shellEscape(value) {
|
|
307
|
-
if (/^[A-Za-z0-9_./:@%+=,-]+$/.test(value)) {
|
|
308
|
-
return value;
|
|
309
|
-
}
|
|
310
|
-
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
311
|
-
}
|
|
312
|
-
function commandToShellString(command) {
|
|
313
|
-
return command.map(shellEscape).join(' ');
|
|
314
|
-
}
|
|
315
|
-
/**
|
|
316
|
-
* Minimal Claude Code plugin manifest written to `<root>/.claude-plugin/plugin.json`
|
|
317
|
-
* in session-install-root mode. Claude's plugin loader treats any directory
|
|
318
|
-
* that contains this file (alongside a `skills/` tree) as a plugin; we pass
|
|
319
|
-
* the root to `claude --plugin-dir <root>` so the session sees exactly the
|
|
320
|
-
* skills we staged — and nothing the repo happens to carry.
|
|
321
|
-
*/
|
|
322
|
-
const SESSION_PLUGIN_MANIFEST = JSON.stringify({
|
|
323
|
-
name: 'agent-workforce-session',
|
|
324
|
-
version: '0.0.0',
|
|
325
|
-
description: 'Ephemeral skills staged by agent-workforce for this session.'
|
|
326
|
-
});
|
|
327
|
-
function buildSessionScaffoldCommand(root) {
|
|
328
|
-
const q = shellEscape(root);
|
|
329
|
-
// mkdir -p creates both the plugin metadata dir and the prpm target.
|
|
330
|
-
// `ln -sfn .claude/skills skills` makes Claude's expected plugin layout
|
|
331
|
-
// (`<root>/skills/<name>/SKILL.md`) resolve to prpm's actual output
|
|
332
|
-
// (`<root>/.claude/skills/<name>/SKILL.md`) without moving any files.
|
|
333
|
-
// The -n guards against following into an existing `skills/` dir (e.g.
|
|
334
|
-
// when the session dir is reused), and -f replaces a prior symlink so
|
|
335
|
-
// the scaffold is idempotent.
|
|
336
|
-
return [
|
|
337
|
-
`mkdir -p ${q}/.claude-plugin ${q}/.claude/skills`,
|
|
338
|
-
`ln -sfn .claude/skills ${q}/skills`,
|
|
339
|
-
`printf '%s' ${shellEscape(SESSION_PLUGIN_MANIFEST)} > ${q}/.claude-plugin/plugin.json`
|
|
340
|
-
].join(' && ');
|
|
341
|
-
}
|
|
342
|
-
function buildInstallArtifacts(plan) {
|
|
343
|
-
if (plan.sessionInstallRoot !== undefined) {
|
|
344
|
-
// Session mode always stages a plugin dir so the caller can pass
|
|
345
|
-
// `--plugin-dir <root>` to claude unconditionally. Even for personas
|
|
346
|
-
// with zero skills, we emit the scaffold (mkdir + manifest + symlink)
|
|
347
|
-
// so the `--plugin-dir` target exists.
|
|
348
|
-
const root = plan.sessionInstallRoot;
|
|
349
|
-
const scaffold = buildSessionScaffoldCommand(root);
|
|
350
|
-
if (plan.installs.length === 0) {
|
|
351
|
-
return {
|
|
352
|
-
installCommand: Object.freeze(['sh', '-c', scaffold]),
|
|
353
|
-
installCommandString: scaffold
|
|
354
|
-
};
|
|
355
|
-
}
|
|
356
|
-
// Chain the raw provider commands after a single `cd <root>` so we emit
|
|
357
|
-
// one shell invocation instead of repeating the cd per skill. Each
|
|
358
|
-
// install.installCommand is already self-contained (`sh -c 'cd <root> &&
|
|
359
|
-
// …'`) for callers who want to run one at a time, but here we flatten
|
|
360
|
-
// to the underlying prpm argv for a cleaner chain.
|
|
361
|
-
const perSkill = plan.installs
|
|
362
|
-
.map((install) => {
|
|
363
|
-
const resolved = resolveSkillSource(install.source);
|
|
364
|
-
const provider = providerFor(resolved.kind);
|
|
365
|
-
return commandToShellString(provider.buildInstallCommand(resolved, plan.harness));
|
|
366
|
-
})
|
|
367
|
-
.join(' && ');
|
|
368
|
-
const installCommandString = `${scaffold} && cd ${shellEscape(root)} && ${perSkill}`;
|
|
369
|
-
return {
|
|
370
|
-
installCommand: Object.freeze(['sh', '-c', installCommandString]),
|
|
371
|
-
installCommandString
|
|
372
|
-
};
|
|
373
|
-
}
|
|
374
|
-
if (plan.installs.length === 0) {
|
|
375
|
-
return {
|
|
376
|
-
installCommand: Object.freeze(['sh', '-c', ':']),
|
|
377
|
-
installCommandString: ':'
|
|
378
|
-
};
|
|
379
|
-
}
|
|
380
|
-
const installCommandString = plan.installs
|
|
381
|
-
.map((install) => commandToShellString(install.installCommand))
|
|
382
|
-
.join(' && ');
|
|
383
|
-
return {
|
|
384
|
-
installCommand: Object.freeze(['sh', '-c', installCommandString]),
|
|
385
|
-
installCommandString
|
|
386
|
-
};
|
|
387
|
-
}
|
|
388
|
-
/**
|
|
389
|
-
* Post-run cleanup: one shell command that removes every ephemeral artifact
|
|
390
|
-
* path declared across all installs in the plan. Runs AFTER the agent step so
|
|
391
|
-
* the agent can still read skill manifests off disk during execution. The
|
|
392
|
-
* provider lockfile is deliberately not in the path set, so repeat runs keep
|
|
393
|
-
* cached resolution.
|
|
394
|
-
*
|
|
395
|
-
* Empty plans return `:` (shell no-op) to keep the post-step shape uniform.
|
|
396
|
-
*/
|
|
397
|
-
function buildCleanupArtifacts(plan) {
|
|
398
|
-
// Session mode: cleanup is the whole stage dir. The scaffold always runs
|
|
399
|
-
// (even for zero-skill personas), so cleanup unconditionally drops the
|
|
400
|
-
// stage dir. The CLI is responsible for removing the enclosing session
|
|
401
|
-
// root; this command covers the install subtree.
|
|
402
|
-
if (plan.sessionInstallRoot !== undefined) {
|
|
403
|
-
const cleanupCommandString = `rm -rf ${shellEscape(plan.sessionInstallRoot)}`;
|
|
404
|
-
return {
|
|
405
|
-
cleanupCommand: Object.freeze(['sh', '-c', cleanupCommandString]),
|
|
406
|
-
cleanupCommandString
|
|
407
|
-
};
|
|
408
|
-
}
|
|
409
|
-
const allPaths = plan.installs.flatMap((install) => [...install.cleanupPaths]);
|
|
410
|
-
const cleanupCommandString = allPaths.length === 0 ? ':' : `rm -rf ${allPaths.map(shellEscape).join(' ')}`;
|
|
411
|
-
return {
|
|
412
|
-
cleanupCommand: Object.freeze(['sh', '-c', cleanupCommandString]),
|
|
413
|
-
cleanupCommandString
|
|
414
|
-
};
|
|
415
|
-
}
|
|
416
|
-
function deepFreeze(value) {
|
|
417
|
-
if (value === null || value === undefined || typeof value !== 'object') {
|
|
418
|
-
return value;
|
|
419
|
-
}
|
|
420
|
-
if (Array.isArray(value)) {
|
|
421
|
-
for (const entry of value) {
|
|
422
|
-
deepFreeze(entry);
|
|
423
|
-
}
|
|
424
|
-
return Object.freeze(value);
|
|
425
|
-
}
|
|
426
|
-
for (const nested of Object.values(value)) {
|
|
427
|
-
deepFreeze(nested);
|
|
428
|
-
}
|
|
429
|
-
return Object.freeze(value);
|
|
430
|
-
}
|
|
431
|
-
function isObject(value) {
|
|
432
|
-
return typeof value === 'object' && value !== null;
|
|
433
|
-
}
|
|
434
|
-
function isHarness(value) {
|
|
435
|
-
return typeof value === 'string' && HARNESS_VALUES.includes(value);
|
|
436
|
-
}
|
|
437
|
-
function isTier(value) {
|
|
438
|
-
return typeof value === 'string' && PERSONA_TIERS.includes(value);
|
|
439
|
-
}
|
|
440
|
-
function isIntent(value) {
|
|
441
|
-
return typeof value === 'string' && PERSONA_INTENTS.includes(value);
|
|
442
|
-
}
|
|
443
|
-
function isTag(value) {
|
|
444
|
-
return typeof value === 'string' && PERSONA_TAGS.includes(value);
|
|
445
|
-
}
|
|
446
|
-
function parseTags(value, context) {
|
|
447
|
-
if (!Array.isArray(value) || value.length === 0) {
|
|
448
|
-
throw new Error(`${context} must be a non-empty array of tags`);
|
|
449
|
-
}
|
|
450
|
-
const out = [];
|
|
451
|
-
for (const [idx, entry] of value.entries()) {
|
|
452
|
-
if (!isTag(entry)) {
|
|
453
|
-
throw new Error(`${context}[${idx}] must be one of: ${PERSONA_TAGS.join(', ')}`);
|
|
454
|
-
}
|
|
455
|
-
if (!out.includes(entry))
|
|
456
|
-
out.push(entry);
|
|
457
|
-
}
|
|
458
|
-
return out;
|
|
459
|
-
}
|
|
460
|
-
function isSidecarMode(value) {
|
|
461
|
-
return typeof value === 'string' && SIDECAR_MD_MODES.includes(value);
|
|
462
|
-
}
|
|
463
|
-
function assertSidecarPath(value, context) {
|
|
464
|
-
if (typeof value !== 'string' || !value.trim()) {
|
|
465
|
-
throw new Error(`${context} must be a non-empty string`);
|
|
466
|
-
}
|
|
467
|
-
if (value.startsWith('/')) {
|
|
468
|
-
throw new Error(`${context} must be a relative POSIX path; got absolute "${value}"`);
|
|
469
|
-
}
|
|
470
|
-
const segments = value.split(/[\\/]+/);
|
|
471
|
-
if (segments.some((s) => s === '..')) {
|
|
472
|
-
throw new Error(`${context} must not contain ".." segments`);
|
|
473
|
-
}
|
|
474
|
-
if (!value.toLowerCase().endsWith('.md')) {
|
|
475
|
-
throw new Error(`${context} must end with .md`);
|
|
476
|
-
}
|
|
477
|
-
}
|
|
478
|
-
function parseHarnessSettings(value, context) {
|
|
479
|
-
if (!isObject(value)) {
|
|
480
|
-
throw new Error(`${context} must be an object`);
|
|
481
|
-
}
|
|
482
|
-
const { reasoning, timeoutSeconds, sandboxMode, approvalPolicy, workspaceWriteNetworkAccess, webSearch } = value;
|
|
483
|
-
if (!['low', 'medium', 'high'].includes(String(reasoning))) {
|
|
484
|
-
throw new Error(`${context}.reasoning must be low|medium|high`);
|
|
485
|
-
}
|
|
486
|
-
if (typeof timeoutSeconds !== 'number' || !Number.isFinite(timeoutSeconds) || timeoutSeconds <= 0) {
|
|
487
|
-
throw new Error(`${context}.timeoutSeconds must be a positive number`);
|
|
488
|
-
}
|
|
489
|
-
const out = {
|
|
490
|
-
reasoning: reasoning,
|
|
491
|
-
timeoutSeconds
|
|
492
|
-
};
|
|
493
|
-
if (sandboxMode !== undefined) {
|
|
494
|
-
if (!CODEX_SANDBOX_MODES.includes(sandboxMode)) {
|
|
495
|
-
throw new Error(`${context}.sandboxMode must be one of: ${CODEX_SANDBOX_MODES.join(', ')}`);
|
|
496
|
-
}
|
|
497
|
-
out.sandboxMode = sandboxMode;
|
|
498
|
-
}
|
|
499
|
-
if (approvalPolicy !== undefined) {
|
|
500
|
-
if (!CODEX_APPROVAL_POLICIES.includes(approvalPolicy)) {
|
|
501
|
-
throw new Error(`${context}.approvalPolicy must be one of: ${CODEX_APPROVAL_POLICIES.join(', ')}`);
|
|
502
|
-
}
|
|
503
|
-
out.approvalPolicy = approvalPolicy;
|
|
504
|
-
}
|
|
505
|
-
if (workspaceWriteNetworkAccess !== undefined) {
|
|
506
|
-
if (typeof workspaceWriteNetworkAccess !== 'boolean') {
|
|
507
|
-
throw new Error(`${context}.workspaceWriteNetworkAccess must be a boolean`);
|
|
508
|
-
}
|
|
509
|
-
out.workspaceWriteNetworkAccess = workspaceWriteNetworkAccess;
|
|
510
|
-
}
|
|
511
|
-
if (webSearch !== undefined) {
|
|
512
|
-
if (typeof webSearch !== 'boolean') {
|
|
513
|
-
throw new Error(`${context}.webSearch must be a boolean`);
|
|
514
|
-
}
|
|
515
|
-
out.webSearch = webSearch;
|
|
516
|
-
}
|
|
517
|
-
return out;
|
|
518
|
-
}
|
|
519
|
-
function parseRuntime(value, context) {
|
|
520
|
-
if (!isObject(value)) {
|
|
521
|
-
throw new Error(`${context} must be an object`);
|
|
522
|
-
}
|
|
523
|
-
const { harness, model, systemPrompt, harnessSettings, claudeMd, claudeMdMode, agentsMd, agentsMdMode, claudeMdContent, agentsMdContent } = value;
|
|
524
|
-
if (!isHarness(harness)) {
|
|
525
|
-
throw new Error(`${context}.harness must be one of: ${HARNESS_VALUES.join(', ')}`);
|
|
526
|
-
}
|
|
527
|
-
if (typeof model !== 'string' || !model.trim()) {
|
|
528
|
-
throw new Error(`${context}.model must be a non-empty string`);
|
|
529
|
-
}
|
|
530
|
-
if (typeof systemPrompt !== 'string' || !systemPrompt.trim()) {
|
|
531
|
-
throw new Error(`${context}.systemPrompt must be a non-empty string`);
|
|
532
|
-
}
|
|
533
|
-
const parsedHarnessSettings = parseHarnessSettings(harnessSettings, `${context}.harnessSettings`);
|
|
534
|
-
if (claudeMd !== undefined)
|
|
535
|
-
assertSidecarPath(claudeMd, `${context}.claudeMd`);
|
|
536
|
-
if (agentsMd !== undefined)
|
|
537
|
-
assertSidecarPath(agentsMd, `${context}.agentsMd`);
|
|
538
|
-
if (claudeMdMode !== undefined && !isSidecarMode(claudeMdMode)) {
|
|
539
|
-
throw new Error(`${context}.claudeMdMode must be one of: ${SIDECAR_MD_MODES.join(', ')}`);
|
|
540
|
-
}
|
|
541
|
-
if (agentsMdMode !== undefined && !isSidecarMode(agentsMdMode)) {
|
|
542
|
-
throw new Error(`${context}.agentsMdMode must be one of: ${SIDECAR_MD_MODES.join(', ')}`);
|
|
543
|
-
}
|
|
544
|
-
// Mode is allowed without a same-level path: a tier may declare just
|
|
545
|
-
// `claudeMdMode` and inherit the path from the spec top-level (or vice
|
|
546
|
-
// versa). The cascade validates that a path/content actually exists at
|
|
547
|
-
// runtime — a stranded mode with no path anywhere becomes a no-op.
|
|
548
|
-
if (claudeMdContent !== undefined && (typeof claudeMdContent !== 'string' || !claudeMdContent.length)) {
|
|
549
|
-
throw new Error(`${context}.claudeMdContent must be a non-empty string`);
|
|
550
|
-
}
|
|
551
|
-
if (agentsMdContent !== undefined && (typeof agentsMdContent !== 'string' || !agentsMdContent.length)) {
|
|
552
|
-
throw new Error(`${context}.agentsMdContent must be a non-empty string`);
|
|
553
|
-
}
|
|
554
|
-
return {
|
|
555
|
-
harness,
|
|
556
|
-
model,
|
|
557
|
-
systemPrompt,
|
|
558
|
-
harnessSettings: parsedHarnessSettings,
|
|
559
|
-
...(typeof claudeMd === 'string' ? { claudeMd } : {}),
|
|
560
|
-
...(claudeMdMode ? { claudeMdMode: claudeMdMode } : {}),
|
|
561
|
-
...(typeof agentsMd === 'string' ? { agentsMd } : {}),
|
|
562
|
-
...(agentsMdMode ? { agentsMdMode: agentsMdMode } : {}),
|
|
563
|
-
...(typeof claudeMdContent === 'string' ? { claudeMdContent } : {}),
|
|
564
|
-
...(typeof agentsMdContent === 'string' ? { agentsMdContent } : {})
|
|
565
|
-
};
|
|
566
|
-
}
|
|
567
|
-
function parseSkills(value, context) {
|
|
568
|
-
if (value === undefined) {
|
|
569
|
-
return [];
|
|
570
|
-
}
|
|
571
|
-
if (!Array.isArray(value)) {
|
|
572
|
-
throw new Error(`${context} must be an array if provided`);
|
|
573
|
-
}
|
|
574
|
-
return value.map((entry, idx) => {
|
|
575
|
-
const entryContext = `${context}[${idx}]`;
|
|
576
|
-
if (!isObject(entry)) {
|
|
577
|
-
throw new Error(`${entryContext} must be an object`);
|
|
578
|
-
}
|
|
579
|
-
const { id, source, description } = entry;
|
|
580
|
-
if (typeof id !== 'string' || !id.trim()) {
|
|
581
|
-
throw new Error(`${entryContext}.id must be a non-empty string`);
|
|
582
|
-
}
|
|
583
|
-
if (typeof source !== 'string' || !source.trim()) {
|
|
584
|
-
throw new Error(`${entryContext}.source must be a non-empty string`);
|
|
585
|
-
}
|
|
586
|
-
if (typeof description !== 'string' || !description.trim()) {
|
|
587
|
-
throw new Error(`${entryContext}.description must be a non-empty string`);
|
|
588
|
-
}
|
|
589
|
-
return { id, source, description };
|
|
590
|
-
});
|
|
591
|
-
}
|
|
592
|
-
function parseStringList(value, context) {
|
|
593
|
-
if (value === undefined)
|
|
594
|
-
return undefined;
|
|
595
|
-
if (!Array.isArray(value)) {
|
|
596
|
-
throw new Error(`${context} must be an array if provided`);
|
|
597
|
-
}
|
|
598
|
-
const parsed = value.map((entry, idx) => {
|
|
599
|
-
if (typeof entry !== 'string' || !entry.trim()) {
|
|
600
|
-
throw new Error(`${context}[${idx}] must be a non-empty string`);
|
|
601
|
-
}
|
|
602
|
-
return entry;
|
|
603
|
-
});
|
|
604
|
-
return parsed.length > 0 ? parsed : undefined;
|
|
605
|
-
}
|
|
606
|
-
function parseMount(value, context) {
|
|
607
|
-
if (value === undefined)
|
|
608
|
-
return undefined;
|
|
609
|
-
if (!isObject(value)) {
|
|
610
|
-
throw new Error(`${context} must be an object if provided`);
|
|
611
|
-
}
|
|
612
|
-
const ignoredPatterns = parseStringList(value.ignoredPatterns, `${context}.ignoredPatterns`);
|
|
613
|
-
const readonlyPatterns = parseStringList(value.readonlyPatterns, `${context}.readonlyPatterns`);
|
|
614
|
-
return ignoredPatterns || readonlyPatterns
|
|
615
|
-
? {
|
|
616
|
-
...(ignoredPatterns ? { ignoredPatterns } : {}),
|
|
617
|
-
...(readonlyPatterns ? { readonlyPatterns } : {})
|
|
618
|
-
}
|
|
619
|
-
: undefined;
|
|
620
|
-
}
|
|
621
|
-
const INPUT_NAME_RE = /^[A-Z_][A-Z0-9_]*$/;
|
|
622
|
-
function assertInputName(name, context) {
|
|
623
|
-
if (!INPUT_NAME_RE.test(name)) {
|
|
624
|
-
throw new Error(`${context} must be an env-style name matching ${INPUT_NAME_RE.source}`);
|
|
625
|
-
}
|
|
626
|
-
}
|
|
627
|
-
function parseInputs(value, context) {
|
|
628
|
-
if (value === undefined)
|
|
629
|
-
return undefined;
|
|
630
|
-
if (!isObject(value)) {
|
|
631
|
-
throw new Error(`${context} must be an object if provided`);
|
|
632
|
-
}
|
|
633
|
-
const out = {};
|
|
634
|
-
for (const [name, raw] of Object.entries(value)) {
|
|
635
|
-
assertInputName(name, `${context}.${name}`);
|
|
636
|
-
if (typeof raw === 'string') {
|
|
637
|
-
if (!raw) {
|
|
638
|
-
throw new Error(`${context}.${name} default must be non-empty`);
|
|
639
|
-
}
|
|
640
|
-
out[name] = { default: raw };
|
|
641
|
-
continue;
|
|
642
|
-
}
|
|
643
|
-
if (!isObject(raw)) {
|
|
644
|
-
throw new Error(`${context}.${name} must be a string default or an object`);
|
|
645
|
-
}
|
|
646
|
-
const { description, env, default: defaultValue, optional } = raw;
|
|
647
|
-
const parsed = {};
|
|
648
|
-
if (description !== undefined) {
|
|
649
|
-
if (typeof description !== 'string' || !description.trim()) {
|
|
650
|
-
throw new Error(`${context}.${name}.description must be a non-empty string if provided`);
|
|
651
|
-
}
|
|
652
|
-
parsed.description = description;
|
|
653
|
-
}
|
|
654
|
-
if (env !== undefined) {
|
|
655
|
-
if (typeof env !== 'string' || !env.trim()) {
|
|
656
|
-
throw new Error(`${context}.${name}.env must be a non-empty string if provided`);
|
|
657
|
-
}
|
|
658
|
-
assertInputName(env, `${context}.${name}.env`);
|
|
659
|
-
parsed.env = env;
|
|
660
|
-
}
|
|
661
|
-
if (defaultValue !== undefined) {
|
|
662
|
-
if (typeof defaultValue !== 'string' || !defaultValue) {
|
|
663
|
-
throw new Error(`${context}.${name}.default must be a non-empty string if provided`);
|
|
664
|
-
}
|
|
665
|
-
parsed.default = defaultValue;
|
|
666
|
-
}
|
|
667
|
-
if (optional !== undefined) {
|
|
668
|
-
if (typeof optional !== 'boolean') {
|
|
669
|
-
throw new Error(`${context}.${name}.optional must be a boolean if provided`);
|
|
670
|
-
}
|
|
671
|
-
if (optional && parsed.default !== undefined) {
|
|
672
|
-
throw new Error(`${context}.${name} cannot set both 'optional: true' and 'default' — pick one (defaults already make an input always-resolved)`);
|
|
673
|
-
}
|
|
674
|
-
parsed.optional = optional;
|
|
675
|
-
}
|
|
676
|
-
out[name] = parsed;
|
|
677
|
-
}
|
|
678
|
-
return Object.keys(out).length > 0 ? out : undefined;
|
|
679
|
-
}
|
|
680
|
-
function parsePersonaSpec(value, expectedIntent) {
|
|
681
|
-
if (!isObject(value)) {
|
|
682
|
-
throw new Error(`persona[${expectedIntent}] must be an object`);
|
|
683
|
-
}
|
|
684
|
-
const { id, intent, tags, description, tiers, defaultTier, skills, inputs, env, mcpServers, permissions, mount, claudeMd, claudeMdMode, agentsMd, agentsMdMode, claudeMdContent, agentsMdContent } = value;
|
|
685
|
-
if (typeof id !== 'string' || !id.trim()) {
|
|
686
|
-
throw new Error(`persona[${expectedIntent}].id must be a non-empty string`);
|
|
687
|
-
}
|
|
688
|
-
if (!isIntent(intent)) {
|
|
689
|
-
throw new Error(`persona[${expectedIntent}].intent is invalid`);
|
|
690
|
-
}
|
|
691
|
-
if (intent !== expectedIntent) {
|
|
692
|
-
throw new Error(`persona[${expectedIntent}] intent mismatch: got ${intent}`);
|
|
693
|
-
}
|
|
694
|
-
const parsedTags = parseTags(tags, `persona[${expectedIntent}].tags`);
|
|
695
|
-
if (typeof description !== 'string' || !description.trim()) {
|
|
696
|
-
throw new Error(`persona[${expectedIntent}].description must be a non-empty string`);
|
|
697
|
-
}
|
|
698
|
-
if (!isObject(tiers)) {
|
|
699
|
-
throw new Error(`persona[${expectedIntent}].tiers must be an object`);
|
|
700
|
-
}
|
|
701
|
-
const parsedTiers = {};
|
|
702
|
-
for (const tier of PERSONA_TIERS) {
|
|
703
|
-
parsedTiers[tier] = parseRuntime(tiers[tier], `persona[${expectedIntent}].tiers.${tier}`);
|
|
704
|
-
}
|
|
705
|
-
let parsedDefaultTier;
|
|
706
|
-
if (defaultTier !== undefined) {
|
|
707
|
-
if (!isTier(defaultTier)) {
|
|
708
|
-
throw new Error(`persona[${expectedIntent}].defaultTier must be one of: ${PERSONA_TIERS.join(', ')}`);
|
|
709
|
-
}
|
|
710
|
-
parsedDefaultTier = defaultTier;
|
|
711
|
-
}
|
|
712
|
-
const parsedSkills = parseSkills(skills, `persona[${expectedIntent}].skills`);
|
|
713
|
-
const parsedInputs = parseInputs(inputs, `persona[${expectedIntent}].inputs`);
|
|
714
|
-
const parsedEnv = parseStringMap(env, `persona[${expectedIntent}].env`);
|
|
715
|
-
const parsedMcpServers = parseMcpServers(mcpServers, `persona[${expectedIntent}].mcpServers`);
|
|
716
|
-
const parsedPermissions = parsePermissions(permissions, `persona[${expectedIntent}].permissions`);
|
|
717
|
-
const parsedMount = parseMount(mount, `persona[${expectedIntent}].mount`);
|
|
718
|
-
if (claudeMd !== undefined)
|
|
719
|
-
assertSidecarPath(claudeMd, `persona[${expectedIntent}].claudeMd`);
|
|
720
|
-
if (agentsMd !== undefined)
|
|
721
|
-
assertSidecarPath(agentsMd, `persona[${expectedIntent}].agentsMd`);
|
|
722
|
-
if (claudeMdMode !== undefined && !isSidecarMode(claudeMdMode)) {
|
|
723
|
-
throw new Error(`persona[${expectedIntent}].claudeMdMode must be one of: ${SIDECAR_MD_MODES.join(', ')}`);
|
|
724
|
-
}
|
|
725
|
-
if (agentsMdMode !== undefined && !isSidecarMode(agentsMdMode)) {
|
|
726
|
-
throw new Error(`persona[${expectedIntent}].agentsMdMode must be one of: ${SIDECAR_MD_MODES.join(', ')}`);
|
|
727
|
-
}
|
|
728
|
-
// Spec-level mode without a spec-level path is allowed — a tier may
|
|
729
|
-
// supply the path while inheriting the mode here. See parseRuntime.
|
|
730
|
-
if (claudeMdContent !== undefined &&
|
|
731
|
-
(typeof claudeMdContent !== 'string' || !claudeMdContent.length)) {
|
|
732
|
-
throw new Error(`persona[${expectedIntent}].claudeMdContent must be a non-empty string`);
|
|
733
|
-
}
|
|
734
|
-
if (agentsMdContent !== undefined &&
|
|
735
|
-
(typeof agentsMdContent !== 'string' || !agentsMdContent.length)) {
|
|
736
|
-
throw new Error(`persona[${expectedIntent}].agentsMdContent must be a non-empty string`);
|
|
737
|
-
}
|
|
738
|
-
return {
|
|
739
|
-
id,
|
|
740
|
-
intent,
|
|
741
|
-
tags: parsedTags,
|
|
742
|
-
description,
|
|
743
|
-
skills: parsedSkills,
|
|
744
|
-
...(parsedInputs ? { inputs: parsedInputs } : {}),
|
|
745
|
-
tiers: parsedTiers,
|
|
746
|
-
...(parsedDefaultTier ? { defaultTier: parsedDefaultTier } : {}),
|
|
747
|
-
...(parsedEnv ? { env: parsedEnv } : {}),
|
|
748
|
-
...(parsedMcpServers ? { mcpServers: parsedMcpServers } : {}),
|
|
749
|
-
...(parsedPermissions ? { permissions: parsedPermissions } : {}),
|
|
750
|
-
...(parsedMount ? { mount: parsedMount } : {}),
|
|
751
|
-
...(typeof claudeMd === 'string' ? { claudeMd } : {}),
|
|
752
|
-
...(claudeMdMode ? { claudeMdMode: claudeMdMode } : {}),
|
|
753
|
-
...(typeof agentsMd === 'string' ? { agentsMd } : {}),
|
|
754
|
-
...(agentsMdMode ? { agentsMdMode: agentsMdMode } : {}),
|
|
755
|
-
...(typeof claudeMdContent === 'string' ? { claudeMdContent } : {}),
|
|
756
|
-
...(typeof agentsMdContent === 'string' ? { agentsMdContent } : {})
|
|
757
|
-
};
|
|
758
|
-
}
|
|
759
|
-
function parsePermissions(value, context) {
|
|
760
|
-
if (value === undefined)
|
|
761
|
-
return undefined;
|
|
762
|
-
if (!isObject(value)) {
|
|
763
|
-
throw new Error(`${context} must be an object if provided`);
|
|
764
|
-
}
|
|
765
|
-
const out = {};
|
|
766
|
-
const { allow, deny, mode } = value;
|
|
767
|
-
if (allow !== undefined) {
|
|
768
|
-
if (!Array.isArray(allow) || allow.some((s) => typeof s !== 'string' || !s.trim())) {
|
|
769
|
-
throw new Error(`${context}.allow must be an array of non-empty strings`);
|
|
770
|
-
}
|
|
771
|
-
out.allow = allow;
|
|
772
|
-
}
|
|
773
|
-
if (deny !== undefined) {
|
|
774
|
-
if (!Array.isArray(deny) || deny.some((s) => typeof s !== 'string' || !s.trim())) {
|
|
775
|
-
throw new Error(`${context}.deny must be an array of non-empty strings`);
|
|
776
|
-
}
|
|
777
|
-
out.deny = deny;
|
|
778
|
-
}
|
|
779
|
-
if (mode !== undefined) {
|
|
780
|
-
if (!PERMISSION_MODES.includes(mode)) {
|
|
781
|
-
throw new Error(`${context}.mode must be one of: ${PERMISSION_MODES.join(', ')}`);
|
|
782
|
-
}
|
|
783
|
-
out.mode = mode;
|
|
784
|
-
}
|
|
785
|
-
return Object.keys(out).length > 0 ? out : undefined;
|
|
786
|
-
}
|
|
787
|
-
function parseStringMap(value, context) {
|
|
788
|
-
if (value === undefined)
|
|
789
|
-
return undefined;
|
|
790
|
-
if (!isObject(value)) {
|
|
791
|
-
throw new Error(`${context} must be an object if provided`);
|
|
792
|
-
}
|
|
793
|
-
const out = {};
|
|
794
|
-
for (const [key, v] of Object.entries(value)) {
|
|
795
|
-
if (typeof v !== 'string') {
|
|
796
|
-
throw new Error(`${context}.${key} must be a string`);
|
|
797
|
-
}
|
|
798
|
-
out[key] = v;
|
|
799
|
-
}
|
|
800
|
-
return out;
|
|
801
|
-
}
|
|
802
|
-
function parseMcpServers(value, context) {
|
|
803
|
-
if (value === undefined)
|
|
804
|
-
return undefined;
|
|
805
|
-
if (!isObject(value)) {
|
|
806
|
-
throw new Error(`${context} must be an object if provided`);
|
|
807
|
-
}
|
|
808
|
-
const out = {};
|
|
809
|
-
for (const [name, raw] of Object.entries(value)) {
|
|
810
|
-
if (!isObject(raw)) {
|
|
811
|
-
throw new Error(`${context}.${name} must be an object`);
|
|
812
|
-
}
|
|
813
|
-
const type = raw.type;
|
|
814
|
-
if (type === 'http' || type === 'sse') {
|
|
815
|
-
if (typeof raw.url !== 'string' || !raw.url.trim()) {
|
|
816
|
-
throw new Error(`${context}.${name}.url must be a non-empty string for type=${type}`);
|
|
817
|
-
}
|
|
818
|
-
const headers = parseStringMap(raw.headers, `${context}.${name}.headers`);
|
|
819
|
-
out[name] = { type, url: raw.url, ...(headers ? { headers } : {}) };
|
|
820
|
-
}
|
|
821
|
-
else if (type === 'stdio') {
|
|
822
|
-
if (typeof raw.command !== 'string' || !raw.command.trim()) {
|
|
823
|
-
throw new Error(`${context}.${name}.command must be a non-empty string for type=stdio`);
|
|
824
|
-
}
|
|
825
|
-
const args = raw.args;
|
|
826
|
-
if (args !== undefined && (!Array.isArray(args) || args.some((a) => typeof a !== 'string'))) {
|
|
827
|
-
throw new Error(`${context}.${name}.args must be an array of strings`);
|
|
828
|
-
}
|
|
829
|
-
const env = parseStringMap(raw.env, `${context}.${name}.env`);
|
|
830
|
-
out[name] = {
|
|
831
|
-
type: 'stdio',
|
|
832
|
-
command: raw.command,
|
|
833
|
-
...(args ? { args: args } : {}),
|
|
834
|
-
...(env ? { env } : {})
|
|
835
|
-
};
|
|
836
|
-
}
|
|
837
|
-
else {
|
|
838
|
-
throw new Error(`${context}.${name}.type must be one of: http, sse, stdio`);
|
|
839
|
-
}
|
|
840
|
-
}
|
|
841
|
-
return out;
|
|
842
|
-
}
|
|
843
4
|
function parseRoutingProfile(value, context) {
|
|
844
5
|
if (!isObject(value)) {
|
|
845
6
|
throw new Error(`${context} must be an object`);
|
|
@@ -894,52 +55,6 @@ function requireBuiltInPersona(intent) {
|
|
|
894
55
|
export const routingProfiles = {
|
|
895
56
|
default: parseRoutingProfile(defaultRoutingProfileJson, 'routingProfiles.default')
|
|
896
57
|
};
|
|
897
|
-
/**
|
|
898
|
-
* Resolve the effective sidecar config for a (spec, tier) pair.
|
|
899
|
-
*
|
|
900
|
-
* Path-or-content resolution: each sidecar (`claude*`, `agents*`) is a
|
|
901
|
-
* single "channel" — its `*Md` and `*MdContent` fields are tied together
|
|
902
|
-
* and travel as a unit through the cascade. If the tier-level runtime
|
|
903
|
-
* declares EITHER `claudeMd` or `claudeMdContent`, the tier owns the
|
|
904
|
-
* channel and the top-level path/content is ignored (otherwise a tier
|
|
905
|
-
* path override would silently lose to inherited inlined content, since
|
|
906
|
-
* downstream consumers prefer Content over a path).
|
|
907
|
-
*
|
|
908
|
-
* Mode resolution: independent — a tier can set just `claudeMdMode` and
|
|
909
|
-
* inherit the top-level path. Defaults to `overwrite` if neither layer
|
|
910
|
-
* sets a mode. Modes are only meaningful when a path/content is present.
|
|
911
|
-
*/
|
|
912
|
-
export function resolveSidecar(spec, tier) {
|
|
913
|
-
const runtime = spec.tiers[tier];
|
|
914
|
-
const tierOwnsClaude = runtime.claudeMd !== undefined || runtime.claudeMdContent !== undefined;
|
|
915
|
-
const tierOwnsAgents = runtime.agentsMd !== undefined || runtime.agentsMdContent !== undefined;
|
|
916
|
-
const claudePath = tierOwnsClaude ? runtime.claudeMd : spec.claudeMd;
|
|
917
|
-
const claudeContent = tierOwnsClaude ? runtime.claudeMdContent : spec.claudeMdContent;
|
|
918
|
-
const agentsPath = tierOwnsAgents ? runtime.agentsMd : spec.agentsMd;
|
|
919
|
-
const agentsContent = tierOwnsAgents ? runtime.agentsMdContent : spec.agentsMdContent;
|
|
920
|
-
return {
|
|
921
|
-
...(claudePath ? { claudeMd: claudePath } : {}),
|
|
922
|
-
...(claudeContent ? { claudeMdContent: claudeContent } : {}),
|
|
923
|
-
claudeMdMode: runtime.claudeMdMode ?? spec.claudeMdMode ?? 'overwrite',
|
|
924
|
-
...(agentsPath ? { agentsMd: agentsPath } : {}),
|
|
925
|
-
...(agentsContent ? { agentsMdContent: agentsContent } : {}),
|
|
926
|
-
agentsMdMode: runtime.agentsMdMode ?? spec.agentsMdMode ?? 'overwrite'
|
|
927
|
-
};
|
|
928
|
-
}
|
|
929
|
-
function sidecarSelectionFields(sidecar) {
|
|
930
|
-
return {
|
|
931
|
-
...(sidecar.claudeMd ? { claudeMd: sidecar.claudeMd } : {}),
|
|
932
|
-
...(sidecar.claudeMdContent ? { claudeMdContent: sidecar.claudeMdContent } : {}),
|
|
933
|
-
...(sidecar.claudeMd || sidecar.claudeMdContent
|
|
934
|
-
? { claudeMdMode: sidecar.claudeMdMode }
|
|
935
|
-
: {}),
|
|
936
|
-
...(sidecar.agentsMd ? { agentsMd: sidecar.agentsMd } : {}),
|
|
937
|
-
...(sidecar.agentsMdContent ? { agentsMdContent: sidecar.agentsMdContent } : {}),
|
|
938
|
-
...(sidecar.agentsMd || sidecar.agentsMdContent
|
|
939
|
-
? { agentsMdMode: sidecar.agentsMdMode }
|
|
940
|
-
: {})
|
|
941
|
-
};
|
|
942
|
-
}
|
|
943
58
|
export function resolvePersona(intent, profile = 'default') {
|
|
944
59
|
const profileSpec = typeof profile === 'string' ? routingProfiles[profile] : profile;
|
|
945
60
|
const rule = profileSpec.intents[intent];
|