@agentworkforce/workload-router 0.14.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,299 +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
- });
190
- test('personas with no optional fields keep them undefined on the selection', () => {
191
- // code-reviewer has no env/mcpServers/permissions in its JSON.
192
- const selection = resolvePersona('review');
193
- assert.equal(selection.env, undefined);
194
- assert.equal(selection.mcpServers, undefined);
195
- assert.equal(selection.permissions, undefined);
196
- });
197
- test('resolves testing personas from the default routing profile', () => {
198
- const testStrategy = resolvePersona('test-strategy');
199
- assert.equal(testStrategy.personaId, 'test-strategist');
200
- assert.equal(testStrategy.tier, 'best-value');
201
- const tdd = resolvePersona('tdd-enforcement');
202
- assert.equal(tdd.personaId, 'tdd-guard');
203
- assert.equal(tdd.tier, 'best-value');
204
- const flake = resolvePersona('flake-investigation');
205
- assert.equal(flake.personaId, 'flake-hunter');
206
- assert.equal(flake.tier, 'best');
207
- assert.equal(flake.runtime.harness, 'codex');
208
- });
209
- test('resolves newly added personas from the default routing profile', () => {
210
- const analyst = resolvePersona('requirements-analysis');
211
- assert.equal(analyst.personaId, 'requirements-analyst');
212
- assert.equal(analyst.tier, 'best-value');
213
- const debuggerSelection = resolvePersona('debugging');
214
- assert.equal(debuggerSelection.personaId, 'debugger');
215
- assert.equal(debuggerSelection.tier, 'best');
216
- assert.equal(debuggerSelection.runtime.harness, 'codex');
217
- const security = resolvePersona('security-review');
218
- assert.equal(security.personaId, 'security-reviewer');
219
- assert.equal(security.tier, 'best');
220
- const docs = resolvePersona('documentation');
221
- assert.equal(docs.personaId, 'technical-writer');
222
- assert.equal(docs.tier, 'best-value');
223
- const verification = resolvePersona('verification');
224
- assert.equal(verification.personaId, 'verifier');
225
- assert.equal(verification.tier, 'best-value');
226
- const opencodeWorkflow = resolvePersona('opencode-workflow-correctness');
227
- assert.equal(opencodeWorkflow.personaId, 'opencode-workflow-specialist');
228
- assert.equal(opencodeWorkflow.tier, 'best');
229
- assert.equal(opencodeWorkflow.runtime.harness, 'codex');
230
- });
231
- test('resolves agent-relay-workflow persona from the default routing profile', () => {
232
- const maker = resolvePersona('agent-relay-workflow');
233
- assert.equal(maker.personaId, 'agent-relay-workflow');
234
- assert.equal(maker.tier, 'best-value');
235
- assert.equal(maker.runtime.harness, 'opencode');
236
- assert.equal(maker.skills.length, 4);
237
- assert.equal(maker.skills[0].id, 'skill.sh/writing-agent-relay-workflows');
238
- assert.equal(maker.skills[3].id, 'prpm/choosing-swarm-patterns');
239
- assert.match(maker.runtime.systemPrompt, /complete workflow source/);
240
- assert.match(maker.runtime.systemPrompt, /GitHub primitive shipping steps/);
241
- assert.match(maker.runtime.systemPrompt, /createGitHubStep/);
242
- });
243
- // removed: writing-agent-relay-workflows persona renamed to agent-relay-workflow
244
- test('resolves relay-orchestrator persona from the default routing profile', () => {
245
- const relay = resolvePersona('relay-orchestrator');
246
- assert.equal(relay.personaId, 'relay-orchestrator');
247
- assert.equal(relay.tier, 'best-value');
248
- assert.equal(relay.runtime.harness, 'opencode');
249
- assert.equal(relay.skills.length, 1);
250
- assert.equal(relay.skills[0].id, 'running-headless-orchestrator');
251
- });
252
- test('resolves anti-slop-auditor with the jscpd skill.sh skill attached', () => {
253
- const auditor = resolvePersona('slop-audit');
254
- assert.equal(auditor.personaId, 'anti-slop-auditor');
255
- assert.equal(auditor.tier, 'best');
256
- assert.equal(auditor.runtime.harness, 'codex');
257
- assert.equal(auditor.skills.length, 1);
258
- assert.equal(auditor.skills[0].id, 'kucherenko/jscpd');
259
- assert.match(auditor.skills[0].source, /github\.com\/kucherenko\/jscpd#jscpd/);
260
- // materializeSkillsFor must not throw — the skill.sh URL must be supported.
261
- const plan = materializeSkillsFor(auditor);
262
- assert.equal(plan.installs.length, 1);
263
- assert.deepEqual(plan.installs[0].installCommand, [
264
- 'npx',
265
- '-y',
266
- 'skills',
267
- 'add',
268
- 'https://github.com/kucherenko/jscpd',
269
- '--skill',
270
- 'jscpd',
271
- '-y'
272
- ]);
64
+ assert.equal(selection.runtime.harnessSettings.sandboxMode, 'workspace-write');
65
+ assert.equal(selection.runtime.harnessSettings.approvalPolicy, 'on-request');
66
+ assert.equal(selection.runtime.harnessSettings.workspaceWriteNetworkAccess, true);
67
+ assert.match(selection.agentsMdContent ?? '', /Do not request network escalation only to complete this fallback/);
68
+ });
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"/);
72
+ });
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/);
273
79
  });
274
80
  test('claude is a recognized harness value', () => {
275
81
  assert.ok(HARNESS_VALUES.includes('claude'));
276
82
  });
277
- test('personas default to an empty skills array when none declared', () => {
278
- const reviewer = personaCatalog.review;
279
- assert.ok(Array.isArray(reviewer.skills));
280
- assert.equal(reviewer.skills.length, 0);
281
- });
282
- test('resolves npm-provenance persona with the trusted publishing skill attached', () => {
283
- const selection = resolvePersona('npm-provenance');
284
- assert.equal(selection.personaId, 'npm-provenance-publisher');
285
- assert.equal(selection.tier, 'best-value');
286
- assert.equal(selection.skills.length, 1);
287
- const [skill] = selection.skills;
288
- assert.equal(skill.id, 'prpm/npm-trusted-publishing');
289
- assert.match(skill.source, /prpm\.dev\/packages\/@prpm\/npm-trusted-publishing/);
290
- assert.match(selection.runtime.systemPrompt, /prpm\/npm-trusted-publishing/);
291
- });
292
- test('resolvePersonaByTier carries persona skills through legacy path', () => {
293
- const selection = resolvePersonaByTier('npm-provenance', 'best');
294
- assert.equal(selection.runtime.harness, 'codex');
295
- assert.equal(selection.skills[0]?.id, 'prpm/npm-trusted-publishing');
296
- });
297
83
  test('HARNESS_SKILL_TARGETS covers every harness value', () => {
298
84
  for (const harness of HARNESS_VALUES) {
299
85
  const target = HARNESS_SKILL_TARGETS[harness];
@@ -302,12 +88,27 @@ test('HARNESS_SKILL_TARGETS covers every harness value', () => {
302
88
  assert.ok(target.dir.length > 0);
303
89
  }
304
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
+ });
305
107
  test('materializeSkills emits a codex-scoped prpm install for a prpm.dev URL', () => {
306
108
  const plan = materializeSkills([
307
109
  {
308
- id: 'prpm/npm-trusted-publishing',
309
- source: 'https://prpm.dev/packages/@prpm/npm-trusted-publishing',
310
- description: 'trusted publishing skill'
110
+ ...prpmSkill,
111
+ source: 'https://prpm.dev/packages/@prpm/npm-trusted-publishing'
311
112
  }
312
113
  ], 'codex');
313
114
  assert.equal(plan.harness, 'codex');
@@ -320,43 +121,29 @@ test('materializeSkills emits a codex-scoped prpm install for a prpm.dev URL', (
320
121
  assert.equal(install.installedManifest, '.agents/skills/npm-trusted-publishing/SKILL.md');
321
122
  });
322
123
  test('materializeSkills routes claude skills to .claude/skills via --as claude', () => {
323
- const plan = materializeSkills([
324
- {
325
- id: 'prpm/npm-trusted-publishing',
326
- source: '@prpm/npm-trusted-publishing',
327
- description: 'bare ref form'
328
- }
329
- ], 'claude');
124
+ const plan = materializeSkills([prpmSkill], 'claude');
330
125
  const [install] = plan.installs;
331
126
  assert.deepEqual([...install.installCommand], ['npx', '-y', 'prpm', 'install', '@prpm/npm-trusted-publishing', '--as', 'claude']);
332
127
  assert.equal(install.installedDir, '.claude/skills/npm-trusted-publishing');
333
128
  });
334
- test('materializeSkillsFor derives an install plan from a resolved persona', () => {
335
- const selection = resolvePersona('npm-provenance');
336
- const plan = materializeSkillsFor(selection);
337
- assert.equal(plan.harness, selection.runtime.harness);
338
- assert.equal(plan.installs.length, 1);
339
- const cmd = plan.installs[0].installCommand.join(' ');
340
- assert.match(cmd, /prpm install @prpm\/npm-trusted-publishing --as /);
341
- });
342
129
  test('materializeSkills emits a skill.sh install for a github#skill source', () => {
343
- const plan = materializeSkills([
344
- {
345
- id: 'skill.sh/find-skills',
346
- source: 'https://github.com/vercel-labs/skills#find-skills',
347
- description: 'skill.sh discovery skill'
348
- }
349
- ], 'claude');
130
+ const plan = materializeSkills([skillShSkill], 'claude');
350
131
  assert.equal(plan.installs.length, 1);
351
132
  const [install] = plan.installs;
352
133
  assert.equal(install.sourceKind, 'skill.sh');
353
134
  assert.equal(install.packageRef, 'https://github.com/vercel-labs/skills#find-skills');
354
- assert.deepEqual([...install.installCommand], ['npx', '-y', 'skills', 'add', 'https://github.com/vercel-labs/skills', '--skill', 'find-skills', '-y']);
355
- // 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
+ ]);
356
145
  assert.equal(install.installedDir, '.agents/skills/find-skills');
357
146
  assert.equal(install.installedManifest, '.agents/skills/find-skills/SKILL.md');
358
- // Cleanup should target every harness symlink + the universal dir, but
359
- // never the lockfile itself.
360
147
  assert.deepEqual([...install.cleanupPaths], [
361
148
  '.agents/skills/find-skills',
362
149
  '.claude/skills/find-skills',
@@ -366,171 +153,163 @@ test('materializeSkills emits a skill.sh install for a github#skill source', ()
366
153
  ]);
367
154
  assert.ok(!install.cleanupPaths.includes('skills-lock.json'));
368
155
  });
369
- test('prpm installs carry a harness-scoped cleanup path (not the lockfile)', () => {
156
+ test('materializeSkills accepts GitHub tree URLs for skill.sh skill directories', () => {
370
157
  const plan = materializeSkills([
371
158
  {
372
- id: 'prpm/npm-trusted-publishing',
373
- source: '@prpm/npm-trusted-publishing',
374
- description: 'bare ref form'
159
+ id: 'nextjs-anti-patterns',
160
+ source: 'https://github.com/wsimmonds/claude-nextjs-skills/tree/main/nextjs-anti-patterns',
161
+ description: 'Next.js anti-pattern guidance'
162
+ },
163
+ {
164
+ id: 'lighthouse-ci-integrator',
165
+ source: 'https://github.com/Dexploarer/hyper-forge/tree/main/.claude/skills/lighthouse-ci-integrator',
166
+ description: 'Lighthouse CI guidance'
375
167
  }
376
- ], 'codex');
168
+ ], 'opencode');
169
+ assert.deepEqual(plan.installs.map((install) => ({
170
+ packageRef: install.packageRef,
171
+ installedDir: install.installedDir,
172
+ command: [...install.installCommand]
173
+ })), [
174
+ {
175
+ packageRef: 'https://github.com/wsimmonds/claude-nextjs-skills/tree/main#nextjs-anti-patterns',
176
+ installedDir: '.agents/skills/nextjs-anti-patterns',
177
+ command: [
178
+ 'npx',
179
+ '-y',
180
+ 'skills',
181
+ 'add',
182
+ 'https://github.com/wsimmonds/claude-nextjs-skills/tree/main',
183
+ '--skill',
184
+ 'nextjs-anti-patterns',
185
+ '-y'
186
+ ]
187
+ },
188
+ {
189
+ packageRef: 'https://github.com/Dexploarer/hyper-forge/tree/main#lighthouse-ci-integrator',
190
+ installedDir: '.agents/skills/lighthouse-ci-integrator',
191
+ command: [
192
+ 'npx',
193
+ '-y',
194
+ 'skills',
195
+ 'add',
196
+ 'https://github.com/Dexploarer/hyper-forge/tree/main',
197
+ '--skill',
198
+ 'lighthouse-ci-integrator',
199
+ '-y'
200
+ ]
201
+ }
202
+ ]);
203
+ });
204
+ test('materializeSkills rejects unsafe skill.sh skill names', () => {
205
+ assert.throws(() => materializeSkills([
206
+ {
207
+ id: 'unsafe',
208
+ source: 'https://github.com/example/skills#../unsafe',
209
+ description: 'unsafe fragment'
210
+ }
211
+ ], 'opencode'), /Unsupported skill source/);
212
+ assert.throws(() => materializeSkills([
213
+ {
214
+ id: 'unsafe',
215
+ source: 'https://github.com/example/skills/tree/main/.hidden',
216
+ description: 'unsafe tree leaf'
217
+ }
218
+ ], 'opencode'), /Unsupported skill source/);
219
+ });
220
+ test('prpm installs carry a harness-scoped cleanup path, not the lockfile', () => {
221
+ const plan = materializeSkills([prpmSkill], 'codex');
377
222
  const [install] = plan.installs;
378
223
  assert.deepEqual([...install.cleanupPaths], ['.agents/skills/npm-trusted-publishing']);
379
224
  assert.ok(!install.cleanupPaths.includes('prpm.lock'));
380
225
  });
381
- test('usePersona install command never embeds cleanup (agent must read skills first)', () => {
382
- // Regression guard: previously buildInstallArtifacts inlined `&& rm -rf` into
383
- // the install step, which ran BEFORE the agent step and deleted skill files
384
- // the agent needed to read. Cleanup now lives on a separate post-agent step
385
- // and on install.cleanupCommandString for Mode B callers.
386
- const context = usePersona('npm-provenance');
226
+ test('useSelection install command never embeds cleanup', () => {
227
+ const context = useSelection(syntheticSelection({ skills: [prpmSkill] }));
387
228
  assert.doesNotMatch(context.install.commandString, /rm -rf/);
388
- 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/);
389
230
  });
390
- test('usePersona exposes a post-run cleanupCommandString targeting skill artifact paths', () => {
391
- const context = usePersona('npm-provenance');
231
+ test('useSelection exposes a post-run cleanupCommandString targeting skill artifacts', () => {
232
+ const context = useSelection(syntheticSelection({ skills: [prpmSkill] }));
392
233
  assert.ok(Array.isArray(context.install.cleanupCommand));
393
234
  assert.equal(context.install.cleanupCommand[0], 'sh');
394
235
  assert.match(context.install.cleanupCommandString, /^rm -rf /);
395
236
  assert.match(context.install.cleanupCommandString, /npm-trusted-publishing/);
396
- // The provider lockfile must never be cleaned — repeat runs depend on it.
397
237
  assert.doesNotMatch(context.install.cleanupCommandString, /prpm\.lock|skills-lock\.json/);
398
238
  });
399
- test('usePersona cleanupCommandString chains paths from every install in the plan', () => {
400
- 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] }));
401
241
  const cleanup = context.install.cleanupCommandString;
402
- // Both the skill.sh symlink set and the prpm per-harness dir should appear
403
- // in a single rm -rf chain.
404
242
  assert.match(cleanup, /^rm -rf /);
405
243
  assert.match(cleanup, /find-skills/);
406
- assert.match(cleanup, /self-improving/);
407
- // Cover every skill.sh harness symlink, not just the universal dir.
244
+ assert.match(cleanup, /npm-trusted-publishing/);
408
245
  assert.match(cleanup, /\.agents\/skills\/find-skills/);
409
246
  assert.match(cleanup, /\.claude\/skills\/find-skills/);
410
247
  assert.match(cleanup, /\.factory\/skills\/find-skills/);
411
248
  assert.match(cleanup, /\.kiro\/skills\/find-skills/);
412
249
  });
413
- test('usePersona cleanupCommandString is a shell no-op when the persona declares no skills', () => {
414
- 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());
415
252
  assert.equal(context.install.cleanupCommandString, ':');
416
253
  });
417
254
  test('materializeSkills with installRoot stages claude skills under the stage dir', () => {
418
255
  const installRoot = '/tmp/agent-workforce/sessions/test-run/claude/plugin';
419
- const plan = materializeSkills([
420
- {
421
- id: 'prpm/npm-trusted-publishing',
422
- source: '@prpm/npm-trusted-publishing',
423
- description: 'bare ref form'
424
- }
425
- ], 'claude', { installRoot });
256
+ const plan = materializeSkills([prpmSkill], 'claude', { installRoot });
426
257
  assert.equal(plan.sessionInstallRoot, installRoot);
427
258
  const [install] = plan.installs;
428
259
  assert.equal(install.installedDir, `${installRoot}/.claude/skills/npm-trusted-publishing`);
429
260
  assert.equal(install.installedManifest, `${installRoot}/.claude/skills/npm-trusted-publishing/SKILL.md`);
430
- // Per-install command is self-contained: runs prpm inside the stage dir.
431
261
  assert.equal(install.installCommand[0], 'sh');
432
262
  assert.equal(install.installCommand[1], '-c');
433
263
  const script = install.installCommand[2];
434
264
  assert.match(script, /^cd /);
435
265
  assert.match(script, /agent-workforce\/sessions\/test-run\/claude\/plugin/);
436
266
  assert.match(script, /npx -y prpm install @prpm\/npm-trusted-publishing --as claude/);
437
- // Per-skill cleanupPaths is empty; cleanup lives at the plan level.
438
267
  assert.deepEqual([...install.cleanupPaths], []);
439
268
  });
440
269
  test('materializeSkills rejects installRoot for non-claude harnesses', () => {
441
- assert.throws(() => materializeSkills([
442
- {
443
- id: 'prpm/x',
444
- source: '@prpm/x',
445
- description: 'x'
446
- }
447
- ], '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/);
448
273
  });
449
- test('useSelection with installRoot emits scaffold + chained prpm in install.commandString', () => {
274
+ test('useSelection with installRoot emits scaffold plus chained prpm', () => {
450
275
  const installRoot = '/tmp/agent-workforce/sessions/scaffold-test/claude/plugin';
451
- const selection = resolvePersonaByTier('npm-provenance', 'best-value');
452
- // Force harness=claude so the installRoot path is exercised regardless of
453
- // the persona's default tier harness.
276
+ const selection = syntheticSelection({ skills: [prpmSkill] });
454
277
  const context = useSelection(selection, { harness: 'claude', installRoot });
455
278
  assert.equal(context.install.plan.sessionInstallRoot, installRoot);
456
279
  const cmd = context.install.commandString;
457
- // Scaffold: the three mkdir/ln/printf steps go first.
458
280
  assert.match(cmd, /^mkdir -p /);
459
281
  assert.match(cmd, /\.claude-plugin/);
460
282
  assert.match(cmd, /ln -sfn \.claude\/skills /);
461
283
  assert.match(cmd, /printf '%s' /);
462
- // Then a single cd into the stage dir, then the prpm call.
463
284
  assert.match(cmd, / && cd '?\/tmp\/agent-workforce\/sessions\/scaffold-test\/claude\/plugin'? && /);
464
285
  assert.match(cmd, /npx -y prpm install @prpm\/npm-trusted-publishing --as claude/);
465
286
  });
466
287
  test('useSelection with installRoot collapses cleanup to a single rm -rf of the stage dir', () => {
467
288
  const installRoot = '/tmp/agent-workforce/sessions/cleanup-test/claude/plugin';
468
- const selection = resolvePersonaByTier('npm-provenance', 'best-value');
469
- const context = useSelection(selection, { harness: 'claude', installRoot });
470
- // shellEscape leaves paths made of [A-Za-z0-9_./:@%+=,-] unquoted.
289
+ const context = useSelection(syntheticSelection({ skills: [prpmSkill] }), { harness: 'claude', installRoot });
471
290
  assert.equal(context.install.cleanupCommandString, `rm -rf /tmp/agent-workforce/sessions/cleanup-test/claude/plugin`);
472
291
  });
473
- test('materializeSkills with installRoot + no skills still reports the sessionInstallRoot', () => {
292
+ test('materializeSkills with installRoot and no skills still reports the sessionInstallRoot', () => {
474
293
  const installRoot = '/tmp/agent-workforce/sessions/empty/claude/plugin';
475
294
  const plan = materializeSkills([], 'claude', { installRoot });
476
295
  assert.equal(plan.sessionInstallRoot, installRoot);
477
296
  assert.equal(plan.installs.length, 0);
478
297
  });
479
- test('useSelection with installRoot + no skills emits scaffold so --plugin-dir target exists', () => {
480
- // Skill-less claude personas (e.g. posthog) still need the stage dir to
481
- // exist so `claude --plugin-dir <installRoot>` finds a valid plugin.
298
+ test('useSelection with installRoot and no skills emits scaffold so plugin dir exists', () => {
482
299
  const installRoot = '/tmp/agent-workforce/sessions/empty-scaffold/claude/plugin';
483
- const selection = resolvePersonaByTier('posthog', 'best');
484
- const context = useSelection(selection, { harness: 'claude', installRoot });
300
+ const context = useSelection(syntheticSelection(), { harness: 'claude', installRoot });
485
301
  assert.equal(context.install.plan.sessionInstallRoot, installRoot);
486
302
  assert.equal(context.install.plan.installs.length, 0);
487
- const cmd = context.install.commandString;
488
- // Must NOT be the `:` no-op; must run the scaffold.
489
- assert.notEqual(cmd, ':');
490
- assert.match(cmd, /^mkdir -p /);
491
- assert.match(cmd, /\.claude-plugin/);
492
- assert.match(cmd, /ln -sfn \.claude\/skills /);
493
- assert.match(cmd, /printf '%s' /);
494
- // 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 /);
495
305
  assert.equal(context.install.cleanupCommandString, `rm -rf ${installRoot}`);
496
306
  });
497
- test('resolves capability-discovery persona carrying both skill.sh and prpm skills', () => {
498
- const selection = resolvePersona('capability-discovery');
499
- assert.equal(selection.personaId, 'capability-discoverer');
500
- assert.equal(selection.tier, 'best-value');
501
- assert.equal(selection.skills.length, 2);
502
- const byId = new Map(selection.skills.map((s) => [s.id, s]));
503
- const skillSh = byId.get('skill.sh/find-skills');
504
- assert.ok(skillSh, 'missing skill.sh/find-skills skill');
505
- assert.equal(skillSh.source, 'https://github.com/vercel-labs/skills#find-skills');
506
- const prpm = byId.get('prpm/self-improving');
507
- assert.ok(prpm, 'missing prpm/self-improving skill');
508
- assert.match(prpm.source, /prpm\.dev\/packages\/@prpm\/self-improving/);
509
- });
510
- test('materializeSkillsFor capability-discovery plans both installs under one shell chain with cleanup', () => {
511
- const selection = resolvePersona('capability-discovery');
512
- const plan = materializeSkillsFor(selection);
513
- assert.equal(plan.installs.length, 2);
514
- const byKind = new Map(plan.installs.map((i) => [i.sourceKind, i]));
515
- const skillShInstall = byKind.get('skill.sh');
516
- const prpmInstall = byKind.get('prpm');
517
- assert.ok(skillShInstall, 'missing skill.sh install');
518
- assert.ok(prpmInstall, 'missing prpm install');
519
- assert.deepEqual([...skillShInstall.installCommand], ['npx', '-y', 'skills', 'add', 'https://github.com/vercel-labs/skills', '--skill', 'find-skills', '-y']);
520
- assert.equal(prpmInstall.packageRef, '@prpm/self-improving');
521
- const context = usePersona('capability-discovery');
522
- const cmd = context.install.commandString;
523
- // Both installs should be chained back-to-back with `&&`, with NO inline
524
- // cleanup — cleanup lives on a separate post-agent step.
525
- assert.match(cmd, /skills add https:\/\/github\.com\/vercel-labs\/skills --skill find-skills -y && npx -y prpm install @prpm\/self-improving/);
526
- assert.doesNotMatch(cmd, /rm -rf/);
527
- });
528
307
  test('materializeSkills rejects unknown skill sources', () => {
529
308
  assert.throws(() => materializeSkills([
530
309
  {
531
310
  id: 'x',
532
311
  source: 'https://example.com/random',
533
- description: 'not a prpm source'
312
+ description: 'not a supported source'
534
313
  }
535
314
  ], 'claude'), /Unsupported skill source/);
536
315
  });
@@ -539,40 +318,20 @@ test('materializeSkills handles personas with no skills', () => {
539
318
  assert.equal(plan.installs.length, 0);
540
319
  });
541
320
  test('usePersona combines selection and grouped install metadata into a frozen context', () => {
542
- const context = usePersona('npm-provenance');
543
- const selection = resolvePersona('npm-provenance');
321
+ const context = usePersona('persona-authoring');
322
+ const selection = resolvePersona('persona-authoring');
544
323
  const plan = materializeSkillsFor(selection);
545
324
  assert.deepEqual(context.selection, selection);
546
325
  assert.deepEqual(context.install.plan, plan);
547
326
  assert.equal(context.install.command[0], 'sh');
548
- assert.match(context.install.commandString, /prpm install/);
327
+ assert.match(context.install.commandString, /skills add/);
549
328
  assert.ok(Object.isFrozen(context));
550
329
  assert.ok(Object.isFrozen(context.selection));
551
330
  assert.ok(Object.isFrozen(context.install));
552
331
  assert.ok(Object.isFrozen(context.install.plan));
553
332
  assert.ok(Object.isFrozen(context.install.command));
554
333
  });
555
- function syntheticSpec(over = {}) {
556
- const baseRuntime = {
557
- harness: 'claude',
558
- model: 'claude-3-5-sonnet',
559
- systemPrompt: 'base',
560
- harnessSettings: { reasoning: 'medium', timeoutSeconds: 300 }
561
- };
562
- return {
563
- id: 's',
564
- intent: 'documentation',
565
- tags: ['documentation'],
566
- description: 'd',
567
- skills: [],
568
- tiers: { best: baseRuntime, 'best-value': baseRuntime, minimum: baseRuntime },
569
- ...over
570
- };
571
- }
572
334
  test('resolveSidecar: tier path override drops top-level inlined content for the same channel', () => {
573
- // Regression for channel mixing: a tier-level claudeMd MUST own the
574
- // channel and exclude top-level claudeMdContent, otherwise downstream
575
- // selection (which prefers Content) silently discards the override.
576
335
  const spec = syntheticSpec({
577
336
  claudeMdContent: '# top-level inlined\n',
578
337
  claudeMdMode: 'overwrite',
@@ -588,11 +347,9 @@ test('resolveSidecar: tier path override drops top-level inlined content for the
588
347
  const resolved = resolveSidecar(spec, 'best');
589
348
  assert.equal(resolved.claudeMd, '/abs/persona.md');
590
349
  assert.equal(resolved.claudeMdContent, undefined);
591
- // Mode still falls back to top-level even though path/content didn't.
592
350
  assert.equal(resolved.claudeMdMode, 'overwrite');
593
351
  });
594
352
  test('resolveSidecar: mode cascades independently of path', () => {
595
- // Top-level claudeMdMode overrides default, tier inherits the path.
596
353
  const spec = syntheticSpec({
597
354
  claudeMd: '/abs/top.md',
598
355
  claudeMdMode: 'extend'
@@ -601,16 +358,13 @@ test('resolveSidecar: mode cascades independently of path', () => {
601
358
  assert.equal(resolved.claudeMd, '/abs/top.md');
602
359
  assert.equal(resolved.claudeMdMode, 'extend');
603
360
  });
604
- test('resolvePersona populates sidecar selection fields from the catalog', () => {
605
- // Built-in personas don't ship sidecars today, so the resolved selection
606
- // has no sidecar fields — but the helper must at least never throw and
607
- // must omit the optional fields cleanly. This is the contract that lets
608
- // a future built-in with claudeMd flow through usePersona without a
609
- // separate resolveSidecar call.
610
- const sel = resolvePersona('documentation');
361
+ test('resolvePersona populates sidecar selection fields from the internal catalog', () => {
362
+ const sel = resolvePersona('persona-authoring');
611
363
  assert.equal(sel.claudeMd, undefined);
612
364
  assert.equal(sel.claudeMdContent, undefined);
613
365
  assert.equal(sel.claudeMdMode, undefined);
614
366
  assert.equal(sel.agentsMd, undefined);
367
+ assert.match(sel.agentsMdContent ?? '', /Persona author/);
368
+ assert.equal(sel.agentsMdMode, 'overwrite');
615
369
  });
616
370
  //# sourceMappingURL=index.test.js.map