@agentworkforce/workload-router 0.15.0 → 0.15.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.
@@ -1,304 +1,85 @@
1
1
  import test from 'node:test';
2
2
  import assert from 'node:assert/strict';
3
- import { HARNESS_SKILL_TARGETS, HARNESS_VALUES, materializeSkills, materializeSkillsFor, personaCatalog, resolvePersona, resolvePersonaByTier, resolveSidecar, usePersona, useSelection } from './index.js';
4
- test('resolves frontend implementer from default routing profile', () => {
5
- const result = resolvePersona('implement-frontend');
6
- assert.equal(result.personaId, 'frontend-implementer');
7
- assert.equal(result.tier, 'best-value');
8
- assert.equal(result.runtime.harness, 'opencode');
9
- assert.match(result.rationale, /balanced-default/);
10
- });
11
- test('resolves review from custom routing profile rule', () => {
12
- const result = resolvePersona('review', {
13
- id: 'fast-review',
14
- description: 'Aggressive low-cost mode for lightweight checks',
15
- intents: {
16
- 'implement-frontend': {
17
- tier: 'minimum',
18
- rationale: 'fast and cheap'
19
- },
20
- review: {
21
- tier: 'minimum',
22
- rationale: 'small PR sanity checks only'
23
- },
24
- 'architecture-plan': {
25
- tier: 'best-value',
26
- rationale: 'still needs decent quality'
27
- },
28
- 'requirements-analysis': {
29
- tier: 'minimum',
30
- rationale: 'quick scope triage is enough here'
31
- },
32
- debugging: {
33
- tier: 'best',
34
- rationale: 'debugging still needs deeper reasoning'
35
- },
36
- 'security-review': {
37
- tier: 'best',
38
- rationale: 'security stays on the strongest tier'
39
- },
40
- documentation: {
41
- tier: 'minimum',
42
- rationale: 'docs tweaks can be short'
43
- },
44
- verification: {
45
- tier: 'best-value',
46
- rationale: 'fresh evidence review needs balanced depth'
47
- },
48
- 'test-strategy': {
49
- tier: 'best-value',
50
- rationale: 'needs balanced coverage planning'
51
- },
52
- 'tdd-enforcement': {
53
- tier: 'minimum',
54
- rationale: 'short process reminders are enough'
55
- },
56
- 'flake-investigation': {
57
- tier: 'best',
58
- rationale: 'deep debugging is worth the cost'
59
- },
60
- 'opencode-workflow-correctness': {
61
- tier: 'best',
62
- rationale: 'cross-layer workflow failures need deeper investigation'
63
- },
64
- 'npm-provenance': {
65
- tier: 'best-value',
66
- rationale: 'mechanical workflow wiring'
67
- },
68
- 'cloud-sandbox-infra': {
69
- tier: 'best',
70
- rationale: 'infra changes need deep reasoning'
71
- },
72
- 'sage-slack-egress-migration': {
73
- tier: 'best-value',
74
- rationale: 'migration wiring can use the balanced default'
75
- },
76
- 'sage-proactive-rewire': {
77
- tier: 'best-value',
78
- rationale: 'rewiring work is configuration-heavy rather than max-depth by default'
79
- },
80
- 'cloud-slack-proxy-guard': {
81
- tier: 'best-value',
82
- rationale: 'proxy guard checks usually fit the balanced default tier'
83
- },
84
- 'sage-cloud-e2e-conduction': {
85
- tier: 'best-value',
86
- rationale: 'e2e conduction benefits from strong reasoning without the highest-cost default'
87
- },
88
- 'capability-discovery': {
89
- tier: 'best-value',
90
- rationale: 'lightweight discovery work'
91
- },
92
- 'npm-package-compat': {
93
- tier: 'best-value',
94
- rationale: 'mechanical package.json audits'
95
- },
96
- posthog: {
97
- tier: 'best-value',
98
- rationale: 'analytics lookups via MCP'
99
- },
100
- 'persona-authoring': {
101
- tier: 'best-value',
102
- rationale: 'scaffolding a persona is mechanical wiring work'
103
- },
104
- 'slop-audit': {
105
- tier: 'minimum',
106
- rationale: 'quick slop sweep is enough here'
107
- },
108
- 'api-contract-review': {
109
- tier: 'best',
110
- rationale: 'breaking-change classification has high blast radius'
111
- },
112
- 'local-stack-orchestration': {
113
- tier: 'best-value',
114
- rationale: 'compose wiring is mechanical given the topology'
115
- },
116
- 'e2e-validation': {
117
- tier: 'best',
118
- rationale: 'hop-by-hop validation is the last line of defense'
119
- },
120
- 'write-integration-tests': {
121
- tier: 'best-value',
122
- rationale: 'integration test template is well-defined'
123
- },
124
- 'agent-relay-workflow': {
125
- tier: 'best-value',
126
- rationale: 'workflow orchestration uses balanced reasoning'
127
- },
128
- 'relay-orchestrator': {
129
- tier: 'best-value',
130
- rationale: 'relay orchestration uses balanced reasoning'
131
- }
132
- }
133
- });
134
- assert.equal(result.personaId, 'code-reviewer');
135
- assert.equal(result.tier, 'minimum');
136
- assert.equal(result.runtime.harness, 'opencode');
137
- });
138
- test('resolves npm-package-compat to npm-package-bundler-guard from default routing profile', () => {
139
- const result = resolvePersona('npm-package-compat');
140
- assert.equal(result.personaId, 'npm-package-bundler-guard');
141
- assert.equal(result.tier, 'best-value');
142
- assert.equal(result.runtime.harness, 'claude');
143
- assert.match(result.rationale, /balanced-default/);
144
- });
145
- test('legacy tier override remains available via resolvePersonaByTier', () => {
146
- const result = resolvePersonaByTier('architecture-plan', 'best');
147
- assert.equal(result.runtime.harness, 'codex');
148
- assert.equal(result.runtime.harnessSettings.reasoning, 'high');
149
- assert.match(result.rationale, /legacy-tier-override/);
150
- });
151
- test('resolvePersona propagates mcpServers and permissions to the selection', () => {
152
- // posthog is the library's canonical carrier for mcpServers + permissions.
153
- // It used to also carry env.POSTHOG_API_KEY, but the persona switched to
154
- // mcp-remote/OAuth (stdio, no env, no bearer header) in e3342c7. Env
155
- // propagation through the loader is covered by the cli package's
156
- // local-personas cascade tests.
157
- const selection = resolvePersona('posthog');
158
- assert.equal(selection.personaId, 'posthog');
159
- // mcpServers carry through with the full stdio shape (command + args).
160
- const posthogServer = selection.mcpServers?.posthog;
161
- assert.ok(posthogServer, 'expected mcpServers.posthog on the selection');
162
- assert.equal(posthogServer.type, 'stdio');
163
- if (posthogServer.type === 'stdio') {
164
- assert.equal(posthogServer.command, 'npx');
165
- assert.deepEqual(posthogServer.args, [
166
- '-y',
167
- 'mcp-remote@latest',
168
- 'https://mcp.posthog.com/mcp'
169
- ]);
170
- }
171
- // permissions.allow is carried without modification.
172
- assert.deepEqual(selection.permissions?.allow, ['mcp__posthog']);
173
- });
174
- test('resolvePersonaByTier also propagates mcpServers / permissions', () => {
175
- const selection = resolvePersonaByTier('posthog', 'minimum');
176
- assert.equal(selection.tier, 'minimum');
177
- assert.ok(selection.mcpServers, 'mcpServers should flow through tier override resolver');
178
- assert.ok(selection.permissions, 'permissions should flow through tier override resolver');
179
- });
180
- test('resolvePersonaByTier propagates persona input declarations', () => {
181
- const selection = resolvePersonaByTier('persona-authoring', 'best');
3
+ import { HARNESS_SKILL_TARGETS, HARNESS_VALUES, PERSONA_INTENTS, listBuiltInPersonas, materializeSkills, materializeSkillsFor, personaCatalog, resolvePersona, resolvePersonaByTier, resolveSidecar, routingProfiles, usePersona, useSelection } from './index.js';
4
+ const prpmSkill = {
5
+ id: 'prpm/npm-trusted-publishing',
6
+ source: '@prpm/npm-trusted-publishing',
7
+ description: 'trusted publishing skill'
8
+ };
9
+ const skillShSkill = {
10
+ id: 'skill.sh/find-skills',
11
+ source: 'https://github.com/vercel-labs/skills#find-skills',
12
+ description: 'skill.sh discovery skill'
13
+ };
14
+ function syntheticSelection(over = {}) {
15
+ const runtime = {
16
+ harness: 'codex',
17
+ model: 'test-model',
18
+ systemPrompt: 'test prompt',
19
+ harnessSettings: { reasoning: 'medium', timeoutSeconds: 300 }
20
+ };
21
+ return {
22
+ personaId: 'synthetic',
23
+ tier: 'best-value',
24
+ runtime,
25
+ skills: [],
26
+ rationale: 'test',
27
+ ...over
28
+ };
29
+ }
30
+ function syntheticSpec(over = {}) {
31
+ const baseRuntime = {
32
+ harness: 'claude',
33
+ model: 'claude-3-5-sonnet',
34
+ systemPrompt: 'base',
35
+ harnessSettings: { reasoning: 'medium', timeoutSeconds: 300 }
36
+ };
37
+ return {
38
+ id: 's',
39
+ intent: 'documentation',
40
+ tags: ['documentation'],
41
+ description: 'd',
42
+ skills: [],
43
+ tiers: { best: baseRuntime, 'best-value': baseRuntime, minimum: baseRuntime },
44
+ ...over
45
+ };
46
+ }
47
+ test('built-in catalog is limited to internal system personas', () => {
48
+ const builtIns = listBuiltInPersonas();
49
+ assert.deepEqual(builtIns.map((p) => p.id), ['persona-maker']);
50
+ assert.equal(personaCatalog['persona-authoring']?.id, 'persona-maker');
51
+ assert.equal(personaCatalog.review, undefined);
52
+ assert.ok(PERSONA_INTENTS.includes('review'));
53
+ assert.equal(routingProfiles.default.intents.review.tier, 'best-value');
54
+ });
55
+ test('resolves persona-maker from the default routing profile', () => {
56
+ const selection = resolvePersona('persona-authoring');
57
+ assert.equal(selection.personaId, 'persona-maker');
58
+ assert.equal(selection.tier, 'best');
59
+ assert.equal(selection.runtime.harness, 'codex');
60
+ assert.match(selection.rationale, /balanced-default/);
182
61
  assert.equal(selection.inputs?.TARGET_DIR?.default, '.agentworkforce/workforce/personas');
183
62
  assert.equal(selection.inputs?.CREATE_MODE?.default, 'local');
184
- // Persona-maker carries its full authoring spec in agentsMdContent; the
185
- // CLI renders input placeholders into the sidecar before materialization,
186
- // so the unrendered selection here still contains the literal `$TARGET_DIR`
187
- // reference.
188
63
  assert.match(selection.agentsMdContent ?? '', /\$TARGET_DIR\/<id>\.json/);
189
64
  assert.equal(selection.runtime.harnessSettings.sandboxMode, 'workspace-write');
190
65
  assert.equal(selection.runtime.harnessSettings.approvalPolicy, 'on-request');
191
66
  assert.equal(selection.runtime.harnessSettings.workspaceWriteNetworkAccess, true);
192
67
  assert.match(selection.agentsMdContent ?? '', /Do not request network escalation only to complete this fallback/);
193
- assert.doesNotMatch(selection.agentsMdContent ?? '', /Check prpm\.dev as a secondary registry when skills\.sh has nothing relevant/);
194
- });
195
- test('personas with no optional fields keep them undefined on the selection', () => {
196
- // code-reviewer has no env/mcpServers/permissions in its JSON.
197
- const selection = resolvePersona('review');
198
- assert.equal(selection.env, undefined);
199
- assert.equal(selection.mcpServers, undefined);
200
- assert.equal(selection.permissions, undefined);
201
68
  });
202
- test('resolves testing personas from the default routing profile', () => {
203
- const testStrategy = resolvePersona('test-strategy');
204
- assert.equal(testStrategy.personaId, 'test-strategist');
205
- assert.equal(testStrategy.tier, 'best-value');
206
- const tdd = resolvePersona('tdd-enforcement');
207
- assert.equal(tdd.personaId, 'tdd-guard');
208
- assert.equal(tdd.tier, 'best-value');
209
- const flake = resolvePersona('flake-investigation');
210
- assert.equal(flake.personaId, 'flake-hunter');
211
- assert.equal(flake.tier, 'best');
212
- assert.equal(flake.runtime.harness, 'codex');
69
+ test('optional pack-owned intents do not resolve from the built-in catalog', () => {
70
+ assert.throws(() => resolvePersona('review'), /No built-in persona is registered for intent "review".*personas-core/);
71
+ assert.throws(() => resolvePersonaByTier('review', 'best'), /No built-in persona is registered for intent "review"/);
213
72
  });
214
- test('resolves newly added personas from the default routing profile', () => {
215
- const analyst = resolvePersona('requirements-analysis');
216
- assert.equal(analyst.personaId, 'requirements-analyst');
217
- assert.equal(analyst.tier, 'best-value');
218
- const debuggerSelection = resolvePersona('debugging');
219
- assert.equal(debuggerSelection.personaId, 'debugger');
220
- assert.equal(debuggerSelection.tier, 'best');
221
- assert.equal(debuggerSelection.runtime.harness, 'codex');
222
- const security = resolvePersona('security-review');
223
- assert.equal(security.personaId, 'security-reviewer');
224
- assert.equal(security.tier, 'best');
225
- const docs = resolvePersona('documentation');
226
- assert.equal(docs.personaId, 'technical-writer');
227
- assert.equal(docs.tier, 'best-value');
228
- const verification = resolvePersona('verification');
229
- assert.equal(verification.personaId, 'verifier');
230
- assert.equal(verification.tier, 'best-value');
231
- const opencodeWorkflow = resolvePersona('opencode-workflow-correctness');
232
- assert.equal(opencodeWorkflow.personaId, 'opencode-workflow-specialist');
233
- assert.equal(opencodeWorkflow.tier, 'best');
234
- assert.equal(opencodeWorkflow.runtime.harness, 'codex');
235
- });
236
- test('resolves agent-relay-workflow persona from the default routing profile', () => {
237
- const maker = resolvePersona('agent-relay-workflow');
238
- assert.equal(maker.personaId, 'agent-relay-workflow');
239
- assert.equal(maker.tier, 'best-value');
240
- assert.equal(maker.runtime.harness, 'opencode');
241
- assert.equal(maker.skills.length, 4);
242
- assert.equal(maker.skills[0].id, 'skill.sh/writing-agent-relay-workflows');
243
- assert.equal(maker.skills[3].id, 'prpm/choosing-swarm-patterns');
244
- assert.match(maker.runtime.systemPrompt, /complete workflow source/);
245
- assert.match(maker.runtime.systemPrompt, /GitHub primitive shipping steps/);
246
- assert.match(maker.runtime.systemPrompt, /createGitHubStep/);
247
- });
248
- // removed: writing-agent-relay-workflows persona renamed to agent-relay-workflow
249
- test('resolves relay-orchestrator persona from the default routing profile', () => {
250
- const relay = resolvePersona('relay-orchestrator');
251
- assert.equal(relay.personaId, 'relay-orchestrator');
252
- assert.equal(relay.tier, 'best-value');
253
- assert.equal(relay.runtime.harness, 'opencode');
254
- assert.equal(relay.skills.length, 1);
255
- assert.equal(relay.skills[0].id, 'running-headless-orchestrator');
256
- });
257
- test('resolves anti-slop-auditor with the jscpd skill.sh skill attached', () => {
258
- const auditor = resolvePersona('slop-audit');
259
- assert.equal(auditor.personaId, 'anti-slop-auditor');
260
- assert.equal(auditor.tier, 'best');
261
- assert.equal(auditor.runtime.harness, 'codex');
262
- assert.equal(auditor.skills.length, 1);
263
- assert.equal(auditor.skills[0].id, 'kucherenko/jscpd');
264
- assert.match(auditor.skills[0].source, /github\.com\/kucherenko\/jscpd#jscpd/);
265
- // materializeSkillsFor must not throw — the skill.sh URL must be supported.
266
- const plan = materializeSkillsFor(auditor);
267
- assert.equal(plan.installs.length, 1);
268
- assert.deepEqual(plan.installs[0].installCommand, [
269
- 'npx',
270
- '-y',
271
- 'skills',
272
- 'add',
273
- 'https://github.com/kucherenko/jscpd',
274
- '--skill',
275
- 'jscpd',
276
- '-y'
277
- ]);
73
+ test('legacy tier override remains available for internal personas', () => {
74
+ const selection = resolvePersonaByTier('persona-authoring', 'minimum');
75
+ assert.equal(selection.personaId, 'persona-maker');
76
+ assert.equal(selection.tier, 'minimum');
77
+ assert.equal(selection.runtime.harness, 'opencode');
78
+ assert.match(selection.rationale, /legacy-tier-override/);
278
79
  });
279
80
  test('claude is a recognized harness value', () => {
280
81
  assert.ok(HARNESS_VALUES.includes('claude'));
281
82
  });
282
- test('personas default to an empty skills array when none declared', () => {
283
- const reviewer = personaCatalog.review;
284
- assert.ok(Array.isArray(reviewer.skills));
285
- assert.equal(reviewer.skills.length, 0);
286
- });
287
- test('resolves npm-provenance persona with the trusted publishing skill attached', () => {
288
- const selection = resolvePersona('npm-provenance');
289
- assert.equal(selection.personaId, 'npm-provenance-publisher');
290
- assert.equal(selection.tier, 'best-value');
291
- assert.equal(selection.skills.length, 1);
292
- const [skill] = selection.skills;
293
- assert.equal(skill.id, 'prpm/npm-trusted-publishing');
294
- assert.match(skill.source, /prpm\.dev\/packages\/@prpm\/npm-trusted-publishing/);
295
- assert.match(selection.runtime.systemPrompt, /prpm\/npm-trusted-publishing/);
296
- });
297
- test('resolvePersonaByTier carries persona skills through legacy path', () => {
298
- const selection = resolvePersonaByTier('npm-provenance', 'best');
299
- assert.equal(selection.runtime.harness, 'codex');
300
- assert.equal(selection.skills[0]?.id, 'prpm/npm-trusted-publishing');
301
- });
302
83
  test('HARNESS_SKILL_TARGETS covers every harness value', () => {
303
84
  for (const harness of HARNESS_VALUES) {
304
85
  const target = HARNESS_SKILL_TARGETS[harness];
@@ -307,12 +88,27 @@ test('HARNESS_SKILL_TARGETS covers every harness value', () => {
307
88
  assert.ok(target.dir.length > 0);
308
89
  }
309
90
  });
91
+ test('materializeSkillsFor derives an install plan from a resolved internal persona', () => {
92
+ const selection = resolvePersona('persona-authoring');
93
+ const plan = materializeSkillsFor(selection);
94
+ assert.equal(plan.harness, 'codex');
95
+ assert.equal(plan.installs.length, 1);
96
+ assert.deepEqual([...plan.installs[0].installCommand], [
97
+ 'npx',
98
+ '-y',
99
+ 'skills',
100
+ 'add',
101
+ 'https://github.com/vercel-labs/skills',
102
+ '--skill',
103
+ 'find-skills',
104
+ '-y'
105
+ ]);
106
+ });
310
107
  test('materializeSkills emits a codex-scoped prpm install for a prpm.dev URL', () => {
311
108
  const plan = materializeSkills([
312
109
  {
313
- id: 'prpm/npm-trusted-publishing',
314
- source: 'https://prpm.dev/packages/@prpm/npm-trusted-publishing',
315
- description: 'trusted publishing skill'
110
+ ...prpmSkill,
111
+ source: 'https://prpm.dev/packages/@prpm/npm-trusted-publishing'
316
112
  }
317
113
  ], 'codex');
318
114
  assert.equal(plan.harness, 'codex');
@@ -325,43 +121,29 @@ test('materializeSkills emits a codex-scoped prpm install for a prpm.dev URL', (
325
121
  assert.equal(install.installedManifest, '.agents/skills/npm-trusted-publishing/SKILL.md');
326
122
  });
327
123
  test('materializeSkills routes claude skills to .claude/skills via --as claude', () => {
328
- const plan = materializeSkills([
329
- {
330
- id: 'prpm/npm-trusted-publishing',
331
- source: '@prpm/npm-trusted-publishing',
332
- description: 'bare ref form'
333
- }
334
- ], 'claude');
124
+ const plan = materializeSkills([prpmSkill], 'claude');
335
125
  const [install] = plan.installs;
336
126
  assert.deepEqual([...install.installCommand], ['npx', '-y', 'prpm', 'install', '@prpm/npm-trusted-publishing', '--as', 'claude']);
337
127
  assert.equal(install.installedDir, '.claude/skills/npm-trusted-publishing');
338
128
  });
339
- test('materializeSkillsFor derives an install plan from a resolved persona', () => {
340
- const selection = resolvePersona('npm-provenance');
341
- const plan = materializeSkillsFor(selection);
342
- assert.equal(plan.harness, selection.runtime.harness);
343
- assert.equal(plan.installs.length, 1);
344
- const cmd = plan.installs[0].installCommand.join(' ');
345
- assert.match(cmd, /prpm install @prpm\/npm-trusted-publishing --as /);
346
- });
347
129
  test('materializeSkills emits a skill.sh install for a github#skill source', () => {
348
- const plan = materializeSkills([
349
- {
350
- id: 'skill.sh/find-skills',
351
- source: 'https://github.com/vercel-labs/skills#find-skills',
352
- description: 'skill.sh discovery skill'
353
- }
354
- ], 'claude');
130
+ const plan = materializeSkills([skillShSkill], 'claude');
355
131
  assert.equal(plan.installs.length, 1);
356
132
  const [install] = plan.installs;
357
133
  assert.equal(install.sourceKind, 'skill.sh');
358
134
  assert.equal(install.packageRef, 'https://github.com/vercel-labs/skills#find-skills');
359
- assert.deepEqual([...install.installCommand], ['npx', '-y', 'skills', 'add', 'https://github.com/vercel-labs/skills', '--skill', 'find-skills', '-y']);
360
- // skill.sh uses a single universal content dir regardless of harness.
135
+ assert.deepEqual([...install.installCommand], [
136
+ 'npx',
137
+ '-y',
138
+ 'skills',
139
+ 'add',
140
+ 'https://github.com/vercel-labs/skills',
141
+ '--skill',
142
+ 'find-skills',
143
+ '-y'
144
+ ]);
361
145
  assert.equal(install.installedDir, '.agents/skills/find-skills');
362
146
  assert.equal(install.installedManifest, '.agents/skills/find-skills/SKILL.md');
363
- // Cleanup should target every harness symlink + the universal dir, but
364
- // never the lockfile itself.
365
147
  assert.deepEqual([...install.cleanupPaths], [
366
148
  '.agents/skills/find-skills',
367
149
  '.claude/skills/find-skills',
@@ -435,171 +217,99 @@ test('materializeSkills rejects unsafe skill.sh skill names', () => {
435
217
  }
436
218
  ], 'opencode'), /Unsupported skill source/);
437
219
  });
438
- test('prpm installs carry a harness-scoped cleanup path (not the lockfile)', () => {
439
- const plan = materializeSkills([
440
- {
441
- id: 'prpm/npm-trusted-publishing',
442
- source: '@prpm/npm-trusted-publishing',
443
- description: 'bare ref form'
444
- }
445
- ], 'codex');
220
+ test('prpm installs carry a harness-scoped cleanup path, not the lockfile', () => {
221
+ const plan = materializeSkills([prpmSkill], 'codex');
446
222
  const [install] = plan.installs;
447
223
  assert.deepEqual([...install.cleanupPaths], ['.agents/skills/npm-trusted-publishing']);
448
224
  assert.ok(!install.cleanupPaths.includes('prpm.lock'));
449
225
  });
450
- test('usePersona install command never embeds cleanup (agent must read skills first)', () => {
451
- // Regression guard: previously buildInstallArtifacts inlined `&& rm -rf` into
452
- // the install step, which ran BEFORE the agent step and deleted skill files
453
- // the agent needed to read. Cleanup now lives on a separate post-agent step
454
- // and on install.cleanupCommandString for Mode B callers.
455
- const context = usePersona('npm-provenance');
226
+ test('useSelection install command never embeds cleanup', () => {
227
+ const context = useSelection(syntheticSelection({ skills: [prpmSkill] }));
456
228
  assert.doesNotMatch(context.install.commandString, /rm -rf/);
457
- assert.match(context.install.commandString, /prpm install @prpm\/npm-trusted-publishing --as [a-z]+/);
229
+ assert.match(context.install.commandString, /prpm install @prpm\/npm-trusted-publishing --as codex/);
458
230
  });
459
- test('usePersona exposes a post-run cleanupCommandString targeting skill artifact paths', () => {
460
- const context = usePersona('npm-provenance');
231
+ test('useSelection exposes a post-run cleanupCommandString targeting skill artifacts', () => {
232
+ const context = useSelection(syntheticSelection({ skills: [prpmSkill] }));
461
233
  assert.ok(Array.isArray(context.install.cleanupCommand));
462
234
  assert.equal(context.install.cleanupCommand[0], 'sh');
463
235
  assert.match(context.install.cleanupCommandString, /^rm -rf /);
464
236
  assert.match(context.install.cleanupCommandString, /npm-trusted-publishing/);
465
- // The provider lockfile must never be cleaned — repeat runs depend on it.
466
237
  assert.doesNotMatch(context.install.cleanupCommandString, /prpm\.lock|skills-lock\.json/);
467
238
  });
468
- test('usePersona cleanupCommandString chains paths from every install in the plan', () => {
469
- const context = usePersona('capability-discovery');
239
+ test('useSelection cleanupCommandString chains paths from every install in the plan', () => {
240
+ const context = useSelection(syntheticSelection({ skills: [skillShSkill, prpmSkill] }));
470
241
  const cleanup = context.install.cleanupCommandString;
471
- // Both the skill.sh symlink set and the prpm per-harness dir should appear
472
- // in a single rm -rf chain.
473
242
  assert.match(cleanup, /^rm -rf /);
474
243
  assert.match(cleanup, /find-skills/);
475
- assert.match(cleanup, /self-improving/);
476
- // Cover every skill.sh harness symlink, not just the universal dir.
244
+ assert.match(cleanup, /npm-trusted-publishing/);
477
245
  assert.match(cleanup, /\.agents\/skills\/find-skills/);
478
246
  assert.match(cleanup, /\.claude\/skills\/find-skills/);
479
247
  assert.match(cleanup, /\.factory\/skills\/find-skills/);
480
248
  assert.match(cleanup, /\.kiro\/skills\/find-skills/);
481
249
  });
482
- test('usePersona cleanupCommandString is a shell no-op when the persona declares no skills', () => {
483
- const context = usePersona('architecture-plan');
250
+ test('useSelection cleanupCommandString is a shell no-op when the persona declares no skills', () => {
251
+ const context = useSelection(syntheticSelection());
484
252
  assert.equal(context.install.cleanupCommandString, ':');
485
253
  });
486
254
  test('materializeSkills with installRoot stages claude skills under the stage dir', () => {
487
255
  const installRoot = '/tmp/agent-workforce/sessions/test-run/claude/plugin';
488
- const plan = materializeSkills([
489
- {
490
- id: 'prpm/npm-trusted-publishing',
491
- source: '@prpm/npm-trusted-publishing',
492
- description: 'bare ref form'
493
- }
494
- ], 'claude', { installRoot });
256
+ const plan = materializeSkills([prpmSkill], 'claude', { installRoot });
495
257
  assert.equal(plan.sessionInstallRoot, installRoot);
496
258
  const [install] = plan.installs;
497
259
  assert.equal(install.installedDir, `${installRoot}/.claude/skills/npm-trusted-publishing`);
498
260
  assert.equal(install.installedManifest, `${installRoot}/.claude/skills/npm-trusted-publishing/SKILL.md`);
499
- // Per-install command is self-contained: runs prpm inside the stage dir.
500
261
  assert.equal(install.installCommand[0], 'sh');
501
262
  assert.equal(install.installCommand[1], '-c');
502
263
  const script = install.installCommand[2];
503
264
  assert.match(script, /^cd /);
504
265
  assert.match(script, /agent-workforce\/sessions\/test-run\/claude\/plugin/);
505
266
  assert.match(script, /npx -y prpm install @prpm\/npm-trusted-publishing --as claude/);
506
- // Per-skill cleanupPaths is empty; cleanup lives at the plan level.
507
267
  assert.deepEqual([...install.cleanupPaths], []);
508
268
  });
509
269
  test('materializeSkills rejects installRoot for non-claude harnesses', () => {
510
- assert.throws(() => materializeSkills([
511
- {
512
- id: 'prpm/x',
513
- source: '@prpm/x',
514
- description: 'x'
515
- }
516
- ], 'codex', { installRoot: '/tmp/agent-workforce/sessions/abc/claude/plugin' }), /installRoot is only supported for the claude harness/);
270
+ assert.throws(() => materializeSkills([prpmSkill], 'codex', {
271
+ installRoot: '/tmp/agent-workforce/sessions/abc/claude/plugin'
272
+ }), /installRoot is only supported for the claude harness/);
517
273
  });
518
- test('useSelection with installRoot emits scaffold + chained prpm in install.commandString', () => {
274
+ test('useSelection with installRoot emits scaffold plus chained prpm', () => {
519
275
  const installRoot = '/tmp/agent-workforce/sessions/scaffold-test/claude/plugin';
520
- const selection = resolvePersonaByTier('npm-provenance', 'best-value');
521
- // Force harness=claude so the installRoot path is exercised regardless of
522
- // the persona's default tier harness.
276
+ const selection = syntheticSelection({ skills: [prpmSkill] });
523
277
  const context = useSelection(selection, { harness: 'claude', installRoot });
524
278
  assert.equal(context.install.plan.sessionInstallRoot, installRoot);
525
279
  const cmd = context.install.commandString;
526
- // Scaffold: the three mkdir/ln/printf steps go first.
527
280
  assert.match(cmd, /^mkdir -p /);
528
281
  assert.match(cmd, /\.claude-plugin/);
529
282
  assert.match(cmd, /ln -sfn \.claude\/skills /);
530
283
  assert.match(cmd, /printf '%s' /);
531
- // Then a single cd into the stage dir, then the prpm call.
532
284
  assert.match(cmd, / && cd '?\/tmp\/agent-workforce\/sessions\/scaffold-test\/claude\/plugin'? && /);
533
285
  assert.match(cmd, /npx -y prpm install @prpm\/npm-trusted-publishing --as claude/);
534
286
  });
535
287
  test('useSelection with installRoot collapses cleanup to a single rm -rf of the stage dir', () => {
536
288
  const installRoot = '/tmp/agent-workforce/sessions/cleanup-test/claude/plugin';
537
- const selection = resolvePersonaByTier('npm-provenance', 'best-value');
538
- const context = useSelection(selection, { harness: 'claude', installRoot });
539
- // shellEscape leaves paths made of [A-Za-z0-9_./:@%+=,-] unquoted.
289
+ const context = useSelection(syntheticSelection({ skills: [prpmSkill] }), { harness: 'claude', installRoot });
540
290
  assert.equal(context.install.cleanupCommandString, `rm -rf /tmp/agent-workforce/sessions/cleanup-test/claude/plugin`);
541
291
  });
542
- test('materializeSkills with installRoot + no skills still reports the sessionInstallRoot', () => {
292
+ test('materializeSkills with installRoot and no skills still reports the sessionInstallRoot', () => {
543
293
  const installRoot = '/tmp/agent-workforce/sessions/empty/claude/plugin';
544
294
  const plan = materializeSkills([], 'claude', { installRoot });
545
295
  assert.equal(plan.sessionInstallRoot, installRoot);
546
296
  assert.equal(plan.installs.length, 0);
547
297
  });
548
- test('useSelection with installRoot + no skills emits scaffold so --plugin-dir target exists', () => {
549
- // Skill-less claude personas (e.g. posthog) still need the stage dir to
550
- // exist so `claude --plugin-dir <installRoot>` finds a valid plugin.
298
+ test('useSelection with installRoot and no skills emits scaffold so plugin dir exists', () => {
551
299
  const installRoot = '/tmp/agent-workforce/sessions/empty-scaffold/claude/plugin';
552
- const selection = resolvePersonaByTier('posthog', 'best');
553
- const context = useSelection(selection, { harness: 'claude', installRoot });
300
+ const context = useSelection(syntheticSelection(), { harness: 'claude', installRoot });
554
301
  assert.equal(context.install.plan.sessionInstallRoot, installRoot);
555
302
  assert.equal(context.install.plan.installs.length, 0);
556
- const cmd = context.install.commandString;
557
- // Must NOT be the `:` no-op; must run the scaffold.
558
- assert.notEqual(cmd, ':');
559
- assert.match(cmd, /^mkdir -p /);
560
- assert.match(cmd, /\.claude-plugin/);
561
- assert.match(cmd, /ln -sfn \.claude\/skills /);
562
- assert.match(cmd, /printf '%s' /);
563
- // Cleanup always drops the stage dir in session mode, even with zero skills.
303
+ assert.notEqual(context.install.commandString, ':');
304
+ assert.match(context.install.commandString, /^mkdir -p /);
564
305
  assert.equal(context.install.cleanupCommandString, `rm -rf ${installRoot}`);
565
306
  });
566
- test('resolves capability-discovery persona carrying both skill.sh and prpm skills', () => {
567
- const selection = resolvePersona('capability-discovery');
568
- assert.equal(selection.personaId, 'capability-discoverer');
569
- assert.equal(selection.tier, 'best-value');
570
- assert.equal(selection.skills.length, 2);
571
- const byId = new Map(selection.skills.map((s) => [s.id, s]));
572
- const skillSh = byId.get('skill.sh/find-skills');
573
- assert.ok(skillSh, 'missing skill.sh/find-skills skill');
574
- assert.equal(skillSh.source, 'https://github.com/vercel-labs/skills#find-skills');
575
- const prpm = byId.get('prpm/self-improving');
576
- assert.ok(prpm, 'missing prpm/self-improving skill');
577
- assert.match(prpm.source, /prpm\.dev\/packages\/@prpm\/self-improving/);
578
- });
579
- test('materializeSkillsFor capability-discovery plans both installs under one shell chain with cleanup', () => {
580
- const selection = resolvePersona('capability-discovery');
581
- const plan = materializeSkillsFor(selection);
582
- assert.equal(plan.installs.length, 2);
583
- const byKind = new Map(plan.installs.map((i) => [i.sourceKind, i]));
584
- const skillShInstall = byKind.get('skill.sh');
585
- const prpmInstall = byKind.get('prpm');
586
- assert.ok(skillShInstall, 'missing skill.sh install');
587
- assert.ok(prpmInstall, 'missing prpm install');
588
- assert.deepEqual([...skillShInstall.installCommand], ['npx', '-y', 'skills', 'add', 'https://github.com/vercel-labs/skills', '--skill', 'find-skills', '-y']);
589
- assert.equal(prpmInstall.packageRef, '@prpm/self-improving');
590
- const context = usePersona('capability-discovery');
591
- const cmd = context.install.commandString;
592
- // Both installs should be chained back-to-back with `&&`, with NO inline
593
- // cleanup — cleanup lives on a separate post-agent step.
594
- assert.match(cmd, /skills add https:\/\/github\.com\/vercel-labs\/skills --skill find-skills -y && npx -y prpm install @prpm\/self-improving/);
595
- assert.doesNotMatch(cmd, /rm -rf/);
596
- });
597
307
  test('materializeSkills rejects unknown skill sources', () => {
598
308
  assert.throws(() => materializeSkills([
599
309
  {
600
310
  id: 'x',
601
311
  source: 'https://example.com/random',
602
- description: 'not a prpm source'
312
+ description: 'not a supported source'
603
313
  }
604
314
  ], 'claude'), /Unsupported skill source/);
605
315
  });
@@ -608,40 +318,20 @@ test('materializeSkills handles personas with no skills', () => {
608
318
  assert.equal(plan.installs.length, 0);
609
319
  });
610
320
  test('usePersona combines selection and grouped install metadata into a frozen context', () => {
611
- const context = usePersona('npm-provenance');
612
- const selection = resolvePersona('npm-provenance');
321
+ const context = usePersona('persona-authoring');
322
+ const selection = resolvePersona('persona-authoring');
613
323
  const plan = materializeSkillsFor(selection);
614
324
  assert.deepEqual(context.selection, selection);
615
325
  assert.deepEqual(context.install.plan, plan);
616
326
  assert.equal(context.install.command[0], 'sh');
617
- assert.match(context.install.commandString, /prpm install/);
327
+ assert.match(context.install.commandString, /skills add/);
618
328
  assert.ok(Object.isFrozen(context));
619
329
  assert.ok(Object.isFrozen(context.selection));
620
330
  assert.ok(Object.isFrozen(context.install));
621
331
  assert.ok(Object.isFrozen(context.install.plan));
622
332
  assert.ok(Object.isFrozen(context.install.command));
623
333
  });
624
- function syntheticSpec(over = {}) {
625
- const baseRuntime = {
626
- harness: 'claude',
627
- model: 'claude-3-5-sonnet',
628
- systemPrompt: 'base',
629
- harnessSettings: { reasoning: 'medium', timeoutSeconds: 300 }
630
- };
631
- return {
632
- id: 's',
633
- intent: 'documentation',
634
- tags: ['documentation'],
635
- description: 'd',
636
- skills: [],
637
- tiers: { best: baseRuntime, 'best-value': baseRuntime, minimum: baseRuntime },
638
- ...over
639
- };
640
- }
641
334
  test('resolveSidecar: tier path override drops top-level inlined content for the same channel', () => {
642
- // Regression for channel mixing: a tier-level claudeMd MUST own the
643
- // channel and exclude top-level claudeMdContent, otherwise downstream
644
- // selection (which prefers Content) silently discards the override.
645
335
  const spec = syntheticSpec({
646
336
  claudeMdContent: '# top-level inlined\n',
647
337
  claudeMdMode: 'overwrite',
@@ -657,11 +347,9 @@ test('resolveSidecar: tier path override drops top-level inlined content for the
657
347
  const resolved = resolveSidecar(spec, 'best');
658
348
  assert.equal(resolved.claudeMd, '/abs/persona.md');
659
349
  assert.equal(resolved.claudeMdContent, undefined);
660
- // Mode still falls back to top-level even though path/content didn't.
661
350
  assert.equal(resolved.claudeMdMode, 'overwrite');
662
351
  });
663
352
  test('resolveSidecar: mode cascades independently of path', () => {
664
- // Top-level claudeMdMode overrides default, tier inherits the path.
665
353
  const spec = syntheticSpec({
666
354
  claudeMd: '/abs/top.md',
667
355
  claudeMdMode: 'extend'
@@ -670,16 +358,13 @@ test('resolveSidecar: mode cascades independently of path', () => {
670
358
  assert.equal(resolved.claudeMd, '/abs/top.md');
671
359
  assert.equal(resolved.claudeMdMode, 'extend');
672
360
  });
673
- test('resolvePersona populates sidecar selection fields from the catalog', () => {
674
- // Built-in personas don't ship sidecars today, so the resolved selection
675
- // has no sidecar fields — but the helper must at least never throw and
676
- // must omit the optional fields cleanly. This is the contract that lets
677
- // a future built-in with claudeMd flow through usePersona without a
678
- // separate resolveSidecar call.
679
- const sel = resolvePersona('documentation');
361
+ test('resolvePersona populates sidecar selection fields from the internal catalog', () => {
362
+ const sel = resolvePersona('persona-authoring');
680
363
  assert.equal(sel.claudeMd, undefined);
681
364
  assert.equal(sel.claudeMdContent, undefined);
682
365
  assert.equal(sel.claudeMdMode, undefined);
683
366
  assert.equal(sel.agentsMd, undefined);
367
+ assert.match(sel.agentsMdContent ?? '', /Persona author/);
368
+ assert.equal(sel.agentsMdMode, 'overwrite');
684
369
  });
685
370
  //# sourceMappingURL=index.test.js.map