@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.
- package/CHANGELOG.md +8 -0
- package/README.md +19 -3
- package/dist/generated/personas.d.ts +5 -1034
- package/dist/generated/personas.d.ts.map +1 -1
- package/dist/generated/personas.js +6 -851
- package/dist/generated/personas.js.map +1 -1
- package/dist/index.d.ts +29 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +111 -57
- package/dist/index.js.map +1 -1
- package/dist/index.test.js +196 -442
- package/dist/index.test.js.map +1 -1
- package/package.json +1 -1
package/dist/index.test.js
CHANGED
|
@@ -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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
assert.
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
assert.equal(
|
|
201
|
-
|
|
202
|
-
assert.equal(
|
|
203
|
-
assert.
|
|
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
|
-
|
|
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], [
|
|
355
|
-
|
|
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('
|
|
156
|
+
test('materializeSkills accepts GitHub tree URLs for skill.sh skill directories', () => {
|
|
370
157
|
const plan = materializeSkills([
|
|
371
158
|
{
|
|
372
|
-
id: '
|
|
373
|
-
source: '
|
|
374
|
-
description: '
|
|
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
|
-
], '
|
|
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('
|
|
382
|
-
|
|
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
|
|
229
|
+
assert.match(context.install.commandString, /prpm install @prpm\/npm-trusted-publishing --as codex/);
|
|
389
230
|
});
|
|
390
|
-
test('
|
|
391
|
-
const context =
|
|
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('
|
|
400
|
-
const context =
|
|
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, /
|
|
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('
|
|
414
|
-
const context =
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
488
|
-
|
|
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
|
|
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('
|
|
543
|
-
const selection = resolvePersona('
|
|
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, /
|
|
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
|
-
|
|
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
|