@agentworkforce/workload-router 0.18.0 → 2.0.1

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