@mistralys/persona-builder 2.2.0 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -22,7 +22,12 @@
22
22
  *
23
23
  * @param text - Template string potentially containing {{> name}} markers
24
24
  * @param partialsMap - Map of partial name → partial content
25
- * @param depth - Current recursion depth (callers should omit; defaults to 0)
25
+ * @param depth - **Internal recursion counter callers must always omit this
26
+ * parameter.** It exists solely so the function can track its own
27
+ * nesting level across recursive calls. Passing a non-zero value
28
+ * bypasses the depth guard (e.g. `resolvePartials(text, map, 5)`
29
+ * expands nothing). This parameter will be removed from the public
30
+ * signature and marked `@internal` in a future release.
26
31
  * @returns The template string with partial markers replaced
27
32
  */
28
33
  declare function resolvePartials(text: string, partialsMap: Record<string, string>, depth?: number): string;
@@ -31,7 +36,8 @@ declare function resolvePartials(text: string, partialsMap: Record<string, strin
31
36
  * conditionals.ts
32
37
  *
33
38
  * Pure template-engine function for resolving conditional blocks.
34
- * Handles {{#if flag}}…{{/if}} and {{#if flag}}…{{else}}…{{/if}} syntax,
39
+ * Handles {{#if flag}}…{{/if}}, {{#if flag}}…{{else}}…{{/if}}, and
40
+ * {{#if flag}}…{{else if flag2}}…{{else}}…{{/if}} (chain) syntax,
35
41
  * including nested {{#if}} blocks inside {{else}} branches.
36
42
  * No file-system I/O.
37
43
  */
@@ -41,10 +47,15 @@ declare function resolvePartials(text: string, partialsMap: Record<string, strin
41
47
  * Syntax:
42
48
  * `{{#if flag}}content{{/if}}`
43
49
  * `{{#if flag}}truthy-content{{else}}falsy-content{{/if}}`
50
+ * `{{#if flag}}truthy-content{{else if flag2}}branch2{{else}}falsy-content{{/if}}`
44
51
  *
45
52
  * Nested conditionals inside `{{else}}` branches are supported:
46
53
  * `{{#if a}}A{{else}}{{#if b}}B{{else}}C{{/if}}{{/if}}`
47
54
  *
55
+ * `{{else if}}` chains are normalised into nested `{{#if}}` blocks before
56
+ * resolution, so they work transparently alongside — and can be combined
57
+ * with — traditional nested `{{#if}}` syntax.
58
+ *
48
59
  * Behaviour:
49
60
  * - When `context[flag]` is truthy: the delimiters are stripped and the
50
61
  * content before `{{else}}` (or the entire inner block if no `{{else}}`)
@@ -309,6 +320,26 @@ interface SuiteConfig {
309
320
  metaSubdir?: string;
310
321
  /** Sub-directory within srcDir that contains content Markdown files. Default: 'content' */
311
322
  contentSubdir?: string;
323
+ /**
324
+ * Optional map of suite-level template variables.
325
+ *
326
+ * These form the **second-lowest** layer (layer 2 of 7) in the merge chain
327
+ * used by `buildContext()`:
328
+ *
329
+ * 1. `BuildConfig.variables` — global defaults (lowest priority)
330
+ * 2. `SuiteConfig.variables` ← this field
331
+ * 3. `_shared.yaml` fields — shared metadata
332
+ * 4. Per-persona YAML fields — per-persona metadata
333
+ * 5. Derived fields — version fallback, tools serialisation, etc.
334
+ * 6. Cross-suite agent map — `agent_<slug>` entries
335
+ * 7. Target flags — `target_<name>` booleans (highest priority)
336
+ *
337
+ * Suite variables override any same-named entry in `BuildConfig.variables`,
338
+ * but are themselves overridden by `_shared.yaml` fields and per-persona
339
+ * YAML metadata. Use this field to inject suite-scoped defaults that apply
340
+ * to every persona in the suite without modifying shared YAML files.
341
+ */
342
+ variables?: Record<string, unknown>;
312
343
  }
313
344
  /**
314
345
  * A single validation outcome returned by a plugin's `onValidate` hook.
@@ -327,10 +358,12 @@ interface ValidationResult {
327
358
  * identification.
328
359
  *
329
360
  * Hook invocation order (per persona):
330
- * 1. onSuiteInit — once per suite, before any persona is built
331
- * 2. onBuildContext — per persona, before template rendering
332
- * 3. onPostRender — per persona, after body rendering
333
- * 4. onValidate — per persona, during the validation phase
361
+ * 1. onSuiteInit — once per suite, before any persona is built
362
+ * 2. onPartials once per suite, after partials are loaded
363
+ * 3. onBuildContext — per persona, before template rendering
364
+ * 4. onPersonaPartials — per persona, before template rendering (after onBuildContext)
365
+ * 5. onPostRender — per persona, after body rendering
366
+ * 6. onValidate — per persona, during the validation phase
334
367
  */
335
368
  interface PersonaBuildPlugin {
336
369
  /**
@@ -347,6 +380,21 @@ interface PersonaBuildPlugin {
347
380
  * @param sharedMeta Shared metadata merged from `_shared.yaml` (mutate in place if needed)
348
381
  */
349
382
  onSuiteInit?(suite: SuiteConfig, sharedMeta: Record<string, unknown>): void;
383
+ /**
384
+ * Called once per suite after partials are loaded from disk (and after any
385
+ * `BuildConfig.partials` inline map has been applied), but before any persona
386
+ * is rendered.
387
+ *
388
+ * Plugins are chained: each plugin receives the accumulated partials map
389
+ * returned by the previous plugin. Return the (possibly mutated or extended)
390
+ * map to pass it to the next plugin.
391
+ *
392
+ * @param partialsMap The current map of partial name → partial content
393
+ * @param suiteName The identifier of the current suite
394
+ * @param suite The suite configuration object
395
+ * @returns Updated partials map (must include all original keys)
396
+ */
397
+ onPartials?(partialsMap: Record<string, string>, suiteName: string, suite: SuiteConfig): Record<string, string>;
350
398
  /**
351
399
  * Called for each persona before template rendering.
352
400
  *
@@ -360,6 +408,34 @@ interface PersonaBuildPlugin {
360
408
  * @returns Updated rendering context (must include all original keys)
361
409
  */
362
410
  onBuildContext?(context: Record<string, unknown>, persona: PersonaMetadata, suite: SuiteConfig, target?: TargetType): Record<string, unknown>;
411
+ /**
412
+ * Called for each persona (and target) after `onBuildContext`, before
413
+ * template rendering.
414
+ *
415
+ * Allows plugins to inject or override partials on a per-persona basis.
416
+ * Plugins are chained: each plugin receives the accumulated partials map
417
+ * returned by the previous plugin. Return the (possibly mutated or extended)
418
+ * map to pass it to the next plugin.
419
+ *
420
+ * **Isolation guarantee:** The builder creates a shallow copy of the
421
+ * suite-level partials map before invoking the first plugin in the chain.
422
+ * The `partialsMap` argument you receive is already persona-scoped — changes
423
+ * to it (or to the map you return) are invisible to other personas in the
424
+ * same suite. Do **not** mutate `partialsMap` in place; instead, return a
425
+ * new map (e.g. `{ ...partialsMap, myPartial: '...' }`) so that each plugin
426
+ * in the chain receives an independent copy of the previous plugin's output.
427
+ *
428
+ * @param partialsMap The current persona-scoped map of partial name → content
429
+ * (a shallow copy of the suite-level map, isolated per persona)
430
+ * @param persona Typed metadata for the persona being built
431
+ * @param context The post-`onBuildContext` rendering context; persona
432
+ * metadata and any context keys injected by `onBuildContext`
433
+ * plugins are accessible here
434
+ * @param suite The suite configuration object
435
+ * @param target The current build target (optional)
436
+ * @returns Updated partials map (must include all original keys)
437
+ */
438
+ onPersonaPartials?(partialsMap: Record<string, string>, persona: PersonaMetadata, context: Record<string, unknown>, suite: SuiteConfig, target?: TargetType): Record<string, string>;
363
439
  /**
364
440
  * Called for each persona after body rendering.
365
441
  *
@@ -489,8 +565,9 @@ declare function loadContent(mdPath: string): Promise<string>;
489
565
  * PersonaBuildPlugin. The runner:
490
566
  * - Skips plugins that do not implement the requested hook (hook is optional)
491
567
  * - Invokes hooks in the order plugins are registered (first-in first-called)
492
- * - For accumulating hooks (onBuildContext, onPostRender), each plugin
493
- * receives the output of the previous plugin as its first argument
568
+ * - For accumulating hooks (onBuildContext, onPartials, onPersonaPartials,
569
+ * onPostRender), each plugin receives the output of the previous plugin
570
+ * as its first argument
494
571
  * - For collecting hooks (onValidate), results are concatenated into a
495
572
  * flat array
496
573
  *
@@ -541,6 +618,44 @@ declare function runBuildContext(plugins: PersonaBuildPlugin[], ctx: Record<stri
541
618
  * @returns Final output string after all plugins have run
542
619
  */
543
620
  declare function runPostRender(plugins: PersonaBuildPlugin[], rendered: string, persona: PersonaMetadata, target: TargetType): string;
621
+ /**
622
+ * Invoke the `onPartials` hook on every registered plugin, accumulating
623
+ * partials map mutations sequentially.
624
+ *
625
+ * Each plugin receives the partials map returned by the previous plugin. If a
626
+ * plugin does not implement `onPartials`, the map passes through unchanged.
627
+ * The final accumulated map is returned.
628
+ *
629
+ * Called once per suite after partials are loaded from disk, before any
630
+ * persona is rendered.
631
+ *
632
+ * @param plugins Ordered list of registered plugins
633
+ * @param partialsMap Initial map of partial name → partial content
634
+ * @param suiteName The identifier of the current suite
635
+ * @param suite The suite configuration object
636
+ * @returns Accumulated partials map after all plugins have run
637
+ */
638
+ declare function runPartials(plugins: PersonaBuildPlugin[], partialsMap: Record<string, string>, suiteName: string, suite: SuiteConfig): Record<string, string>;
639
+ /**
640
+ * Invoke the `onPersonaPartials` hook on every registered plugin, accumulating
641
+ * partials map mutations sequentially.
642
+ *
643
+ * Each plugin receives the partials map returned by the previous plugin. If a
644
+ * plugin does not implement `onPersonaPartials`, the map passes through
645
+ * unchanged. The final accumulated map is returned.
646
+ *
647
+ * Called for each persona (and target) after `onBuildContext`, before
648
+ * template rendering.
649
+ *
650
+ * @param plugins Ordered list of registered plugins
651
+ * @param partialsMap Initial map of partial name → partial content
652
+ * @param persona Typed metadata for the persona being built
653
+ * @param context The rendering context built by `onBuildContext`
654
+ * @param suite The suite configuration object
655
+ * @param target The current build target (optional)
656
+ * @returns Accumulated partials map after all plugins have run
657
+ */
658
+ declare function runPersonaPartials(plugins: PersonaBuildPlugin[], partialsMap: Record<string, string>, persona: PersonaMetadata, context: Record<string, unknown>, suite: SuiteConfig, target?: TargetType): Record<string, string>;
544
659
  /**
545
660
  * Invoke the `onValidate` hook on every registered plugin and collect all
546
661
  * returned ValidationResult objects into a single flat array.
@@ -739,9 +854,19 @@ interface BuildConfig {
739
854
  */
740
855
  suites: Record<string, SuiteConfig>;
741
856
  /**
742
- * Absolute path to the shared partials directory. When provided, partials
743
- * from this directory are loaded as the base layer before suite-local
744
- * partials are overlaid. Optional.
857
+ * Absolute path to the shared partials directory.
858
+ *
859
+ * When provided, partials from this directory form the **second** layer in
860
+ * the five-layer partials resolution order:
861
+ *
862
+ * 1. `BuildConfig.partials` — lowest precedence (inline map)
863
+ * 2. `sharedPartialsDir` — overlaid on top of inline partials (this field)
864
+ * 3. Suite-local partials — overlaid on top of shared partials
865
+ * 4. `onPartials` hooks — suite-level plugin-injected partials
866
+ * 5. `onPersonaPartials` — per-persona overrides (highest precedence)
867
+ *
868
+ * Later layers overlay earlier ones; a key present in multiple layers uses
869
+ * the value from the highest-precedence layer.
745
870
  */
746
871
  sharedPartialsDir?: string;
747
872
  /**
@@ -788,6 +913,43 @@ interface BuildConfig {
788
913
  * `TargetDefinition.defaultFrontmatter` are used.
789
914
  */
790
915
  frontmatter?: Record<string, string>;
916
+ /**
917
+ * Optional map of global template variables made available to every persona
918
+ * during rendering.
919
+ *
920
+ * These form the **lowest-priority** layer (layer 1 of 7) in the merge chain
921
+ * used by `buildContext()`:
922
+ *
923
+ * 1. `BuildConfig.variables` ← this field (lowest priority)
924
+ * 2. `SuiteConfig.variables` — suite-level overrides
925
+ * 3. `_shared.yaml` fields — shared metadata
926
+ * 4. Per-persona YAML fields — per-persona metadata
927
+ * 5. Derived fields — version fallback, tools serialisation, etc.
928
+ * 6. Cross-suite agent map — `agent_<slug>` entries
929
+ * 7. Target flags — `target_<name>` booleans (highest priority)
930
+ *
931
+ * Any key set here that also appears in a higher-priority layer will be
932
+ * overridden. Use this field for project-wide defaults that individual suites
933
+ * or personas can selectively override via `SuiteConfig.variables` or their
934
+ * own YAML metadata.
935
+ */
936
+ variables?: Record<string, unknown>;
937
+ /**
938
+ * Optional map of inline partials, keyed by partial name.
939
+ *
940
+ * These form the **lowest** layer in the five-layer partials resolution
941
+ * order:
942
+ *
943
+ * 1. `BuildConfig.partials` — lowest precedence (this field)
944
+ * 2. `sharedPartialsDir` — overlaid on top of inline partials
945
+ * 3. Suite-local partials — overlaid on top of shared partials
946
+ * 4. `onPartials` hooks — suite-level plugin-injected partials
947
+ * 5. `onPersonaPartials` — per-persona overrides (highest precedence)
948
+ *
949
+ * Later layers overlay earlier ones; a key present in multiple layers uses
950
+ * the value from the highest-precedence layer.
951
+ */
952
+ partials?: Record<string, string>;
791
953
  /**
792
954
  * Optional target registry to use for this build.
793
955
  *
@@ -909,6 +1071,21 @@ declare function renderFrontmatter(template: string, context: Record<string, unk
909
1071
  * agent name map, then iterates all suites × targets, calls
910
1072
  * buildSuite() for each combination, and returns a BuildSummary.
911
1073
  * Respects --check (no writes) and --strict (fail on warnings/errors).
1074
+ *
1075
+ * Template variable injection (7-layer merge order, later layers win):
1076
+ *
1077
+ * 1. BuildConfig.variables — global defaults; available to every suite
1078
+ * 2. SuiteConfig.variables — suite-level overrides
1079
+ * 3. _shared.yaml fields — shared metadata for the suite
1080
+ * 4. Per-persona YAML fields — per-persona metadata
1081
+ * 5. Derived fields — version fallback, tools serialisation, etc.
1082
+ * 6. Cross-suite agent map — agent_<slug> / agent_slug_<slug> entries
1083
+ * 7. Target flags — target_<name> booleans; always highest precedence
1084
+ *
1085
+ * Callers inject global or suite-scoped variables via `BuildConfig.variables`
1086
+ * and `SuiteConfig.variables` respectively. Both fields are forwarded by
1087
+ * buildPersona() to the internal buildContext() function as the two
1088
+ * lowest-priority layers in the merge chain.
912
1089
  */
913
1090
 
914
1091
  /**
@@ -918,22 +1095,39 @@ declare function renderFrontmatter(template: string, context: Record<string, unk
918
1095
  * 1. Load sharedMeta + personaMeta (callers supply pre-loaded values)
919
1096
  * 2. Build merged context
920
1097
  * 3. Run onBuildContext plugin hooks (context accumulation)
921
- * 4. Resolve frontmatter template render frontmatter
922
- * 5. Load content template
923
- * 6. Render body: partials → conditionals → variables → post-process
924
- * 7. Assemble final output (frontmatter + body)
925
- * 8. Run onPostRender plugin hooks (output chain)
926
- * 9. Run onValidate plugin hooks (validation collection)
927
- * 10. Determine output file path
928
- * 11. Write output file (unless check mode)
929
- * 12. Return BuildResult
1098
+ * 4. Run onPersonaPartials plugin hooks (shallow-copy partials map, persona-scoped)
1099
+ * 5. Render frontmatter template → render frontmatter
1100
+ * 6. Load content template
1101
+ * 7. Render body: partials conditionals → variables → post-process
1102
+ * 8. Assemble final output (frontmatter + body)
1103
+ * 9. Run onPostRender plugin hooks (output chain)
1104
+ * 10. Run onValidate plugin hooks (validation collection)
1105
+ * 11. Determine output file path
1106
+ * 12. Write output file (unless check mode)
1107
+ * 13. Return BuildResult
930
1108
  *
931
1109
  * @param personaYamlPath Absolute path to the persona YAML source file
932
1110
  * @param suiteName Identifier for the suite this persona belongs to
933
- * @param suiteConfig Suite configuration object
1111
+ * @param suiteConfig Suite configuration object. `suiteConfig.variables`
1112
+ * is forwarded to `buildContext()` as the
1113
+ * `suiteVariables` layer (layer 2 of 7) — these values
1114
+ * override any `config.variables` but are themselves
1115
+ * overridden by `_shared.yaml` and per-persona fields.
934
1116
  * @param sharedMeta Pre-loaded `_shared.yaml` contents
935
- * @param partialsMap Pre-loaded partials map (shared + suite-local merged)
936
- * @param config Top-level BuildConfig
1117
+ * @param partialsMap Pre-loaded partials map (shared + suite-local merged).
1118
+ * This map is **not** passed directly to rendering.
1119
+ * Instead, a shallow copy (`{ ...partialsMap }`) is
1120
+ * created at step 3b and passed to the `onPersonaPartials`
1121
+ * plugin hooks — the hooks' accumulated output map is what
1122
+ * reaches `resolvePartials`, not `partialsMap` itself.
1123
+ * This ensures that persona-level overrides or injections
1124
+ * do not leak back into the caller's reference or into
1125
+ * subsequent personas in the same suite.
1126
+ * @param config Top-level BuildConfig. `config.variables` is
1127
+ * forwarded to `buildContext()` as the
1128
+ * `configVariables` layer (layer 1 of 7, lowest
1129
+ * priority) — global defaults available to every
1130
+ * persona across all suites.
937
1131
  * @param plugins Registered plugins
938
1132
  * @param target Target output format
939
1133
  * @param agentMap Pre-built cross-suite agent name map
@@ -952,10 +1146,11 @@ declare function buildPersona(personaYamlPath: string, suiteName: string, suiteC
952
1146
  *
953
1147
  * Pipeline:
954
1148
  * 1. Load `_shared.yaml` for the suite
955
- * 2. Load merged partials (shared → suite-local)
1149
+ * 2. Load merged partials (config.partials → shared → suite-local)
956
1150
  * 3. Run `onSuiteInit` on all plugins
957
- * 4. Discover all persona YAML files
958
- * 5. Call `buildPersona()` for each
1151
+ * 4. Run `onPartials` on all plugins (highest priority: may override any file-based partial)
1152
+ * 5. Discover all persona YAML files
1153
+ * 6. Call `buildPersona()` for each
959
1154
  *
960
1155
  * @param suiteName Identifier for this suite
961
1156
  * @param suiteConfig Suite configuration
@@ -1113,4 +1308,4 @@ declare const defaultRegistry: TargetRegistry;
1113
1308
 
1114
1309
  declare const VERSION: string;
1115
1310
 
1116
- export { type BuildConfig, type BuildResult, type BuildSummary, DEFAULT_FRONTMATTER_CLAUDE_CODE, DEFAULT_FRONTMATTER_DEEP_AGENTS, DEFAULT_FRONTMATTER_VSCODE, type PersonaBuildPlugin, type PersonaMetadata, type SuiteConfig, TARGET_CLAUDE_CODE, TARGET_DEEP_AGENTS, TARGET_VSCODE, type TargetDefinition, TargetRegistry, type TargetType, VERSION, type ValidationResult, build, buildPersona, buildSuite, collapseBlankLines, defaultRegistry, discoverPersonaYamls, ensureBlankLineBeforeHeadings, escapeRegExp, loadContent, loadMetadata, loadPartials, normalizeNewlines, renderFrontmatter, resolveConditionals, resolveFrontmatterTemplate, resolvePartials, resolveVariables, runBuildContext, runPostRender, runSuiteInit, runValidate, serializeTools, serializeToolsList, validateFileName, validateStrictMarkers };
1311
+ export { type BuildConfig, type BuildResult, type BuildSummary, DEFAULT_FRONTMATTER_CLAUDE_CODE, DEFAULT_FRONTMATTER_DEEP_AGENTS, DEFAULT_FRONTMATTER_VSCODE, type PersonaBuildPlugin, type PersonaMetadata, type SuiteConfig, TARGET_CLAUDE_CODE, TARGET_DEEP_AGENTS, TARGET_VSCODE, type TargetDefinition, TargetRegistry, type TargetType, VERSION, type ValidationResult, build, buildPersona, buildSuite, collapseBlankLines, defaultRegistry, discoverPersonaYamls, ensureBlankLineBeforeHeadings, escapeRegExp, loadContent, loadMetadata, loadPartials, normalizeNewlines, renderFrontmatter, resolveConditionals, resolveFrontmatterTemplate, resolvePartials, resolveVariables, runBuildContext, runPartials, runPersonaPartials, runPostRender, runSuiteInit, runValidate, serializeTools, serializeToolsList, validateFileName, validateStrictMarkers };
package/dist/index.js CHANGED
@@ -19,10 +19,30 @@ function resolvePartials(text, partialsMap, depth = 0) {
19
19
  }
20
20
 
21
21
  // src/engine/conditionals.ts
22
+ var NO_NESTED_IF = String.raw`(?:(?!\{\{#if\b)[\s\S])*?`;
23
+ var ELSE_IF_PATTERN = new RegExp(
24
+ String.raw`\{\{else if (\w+)\}\}(${NO_NESTED_IF})\{\{\/if\}\}`,
25
+ "g"
26
+ );
27
+ function resolveElseIf(text) {
28
+ if (!text.includes("{{else if ")) {
29
+ return text;
30
+ }
31
+ let result = text;
32
+ let prev;
33
+ do {
34
+ prev = result;
35
+ result = result.replace(
36
+ ELSE_IF_PATTERN,
37
+ (_match, flag, content) => `{{else}}{{#if ${flag}}}${content}{{/if}}{{/if}}`
38
+ );
39
+ } while (result !== prev);
40
+ return result;
41
+ }
22
42
  function resolveConditionals(text, context) {
23
- const noNestedIf = String.raw`(?:(?!\{\{#if\b)[\s\S])*?`;
43
+ const normalized = resolveElseIf(text);
24
44
  const pattern = new RegExp(
25
- String.raw`\n*\{\{#if (\w+)\}\}(${noNestedIf})` + String.raw`(?:\{\{else\}\}(${noNestedIf}))?\{\{\/if\}\}\n*`,
45
+ String.raw`\n*\{\{#if (\w+)\}\}(${NO_NESTED_IF})` + String.raw`(?:\{\{else\}\}(${NO_NESTED_IF}))?\{\{\/if\}\}\n*`,
26
46
  "g"
27
47
  );
28
48
  const resolve = (_match, flag, inner, elseInner) => {
@@ -34,7 +54,7 @@ function resolveConditionals(text, context) {
34
54
  }
35
55
  return "\n";
36
56
  };
37
- let result = text;
57
+ let result = normalized;
38
58
  let prev;
39
59
  do {
40
60
  prev = result;
@@ -129,7 +149,7 @@ function runBuildContext(plugins, ctx, persona, suite, target) {
129
149
  let accumulated = ctx;
130
150
  for (const plugin of plugins) {
131
151
  if (typeof plugin.onBuildContext === "function") {
132
- accumulated = plugin.onBuildContext(accumulated, persona, suite, target);
152
+ accumulated = plugin.onBuildContext(accumulated, persona, suite, target) ?? accumulated;
133
153
  }
134
154
  }
135
155
  return accumulated;
@@ -138,11 +158,29 @@ function runPostRender(plugins, rendered, persona, target) {
138
158
  let output = rendered;
139
159
  for (const plugin of plugins) {
140
160
  if (typeof plugin.onPostRender === "function") {
141
- output = plugin.onPostRender(output, persona, target);
161
+ output = plugin.onPostRender(output, persona, target) ?? output;
142
162
  }
143
163
  }
144
164
  return output;
145
165
  }
166
+ function runPartials(plugins, partialsMap, suiteName, suite) {
167
+ let accumulated = partialsMap;
168
+ for (const plugin of plugins) {
169
+ if (typeof plugin.onPartials === "function") {
170
+ accumulated = plugin.onPartials(accumulated, suiteName, suite) ?? accumulated;
171
+ }
172
+ }
173
+ return accumulated;
174
+ }
175
+ function runPersonaPartials(plugins, partialsMap, persona, context, suite, target) {
176
+ let accumulated = partialsMap;
177
+ for (const plugin of plugins) {
178
+ if (typeof plugin.onPersonaPartials === "function") {
179
+ accumulated = plugin.onPersonaPartials(accumulated, persona, context, suite, target) ?? accumulated;
180
+ }
181
+ }
182
+ return accumulated;
183
+ }
146
184
  function runValidate(plugins, persona, suite, target) {
147
185
  const results = [];
148
186
  for (const plugin of plugins) {
@@ -365,9 +403,20 @@ async function buildAgentNameMap(config) {
365
403
  }
366
404
  return agentMap;
367
405
  }
368
- function buildContext(personaMeta, sharedMeta, agentMap = {}, target, registry) {
406
+ function buildContext(options) {
407
+ const {
408
+ personaMeta,
409
+ sharedMeta,
410
+ agentMap = {},
411
+ target,
412
+ registry,
413
+ configVariables,
414
+ suiteVariables
415
+ } = options;
369
416
  const version = typeof personaMeta["version"] === "string" ? personaMeta["version"] : typeof sharedMeta["default_version"] === "string" ? sharedMeta["default_version"] : "0.0.0";
370
417
  const merged = {
418
+ ...configVariables ?? {},
419
+ ...suiteVariables ?? {},
371
420
  ...sharedMeta,
372
421
  ...personaMeta,
373
422
  version
@@ -422,16 +471,32 @@ function buildContext(personaMeta, sharedMeta, agentMap = {}, target, registry)
422
471
  }
423
472
  async function buildPersona(personaYamlPath, suiteName, suiteConfig, sharedMeta, partialsMap, config, plugins, target, agentMap = {}, registry = defaultRegistry) {
424
473
  const personaMeta = await loadPersonaYaml(personaYamlPath);
425
- let context = buildContext(personaMeta, sharedMeta, agentMap, target, registry);
474
+ let context = buildContext({
475
+ personaMeta,
476
+ sharedMeta,
477
+ agentMap,
478
+ target,
479
+ registry,
480
+ configVariables: config.variables,
481
+ suiteVariables: suiteConfig.variables
482
+ });
426
483
  const personaMetaTyped = personaMeta;
427
484
  context = runBuildContext(plugins, context, personaMetaTyped, suiteConfig, target);
485
+ const personaPartialsMap = runPersonaPartials(
486
+ plugins,
487
+ { ...partialsMap },
488
+ personaMetaTyped,
489
+ context,
490
+ suiteConfig,
491
+ target
492
+ );
428
493
  const fmTemplate = resolveFrontmatterTemplate(target, plugins, config.frontmatter, registry);
429
494
  const contentBasename = path4.basename(personaYamlPath, ".yaml") + ".md";
430
495
  const frontmatter = renderFrontmatter(fmTemplate, context, contentBasename);
431
496
  const contentSubdir = suiteConfig.contentSubdir ?? "content";
432
497
  const contentPath = path4.join(suiteConfig.srcDir, contentSubdir, contentBasename);
433
498
  const bodyTemplate = normalizeNewlines(await readFile(contentPath, "utf8"));
434
- let body = resolvePartials(bodyTemplate, partialsMap);
499
+ let body = resolvePartials(bodyTemplate, personaPartialsMap);
435
500
  body = resolveConditionals(body, context);
436
501
  body = resolveVariables(body, context, contentBasename);
437
502
  body = collapseBlankLines(body);
@@ -469,7 +534,7 @@ async function buildSuite(suiteName, suiteConfig, config, plugins, target, agent
469
534
  const metaSubdir = suiteConfig.metaSubdir ?? "meta";
470
535
  const sharedYamlPath = path4.join(suiteConfig.srcDir, metaSubdir, "_shared.yaml");
471
536
  const sharedMeta = await loadRawYaml(sharedYamlPath);
472
- let partialsMap = {};
537
+ let partialsMap = { ...config.partials ?? {} };
473
538
  if (config.sharedPartialsDir && existsSync(config.sharedPartialsDir)) {
474
539
  partialsMap = { ...partialsMap, ...await loadPartials(config.sharedPartialsDir) };
475
540
  }
@@ -479,6 +544,7 @@ async function buildSuite(suiteName, suiteConfig, config, plugins, target, agent
479
544
  partialsMap = { ...partialsMap, ...await loadPartials(suitePartialsDir) };
480
545
  }
481
546
  runSuiteInit(plugins, suiteConfig, sharedMeta);
547
+ partialsMap = runPartials(plugins, partialsMap, suiteName, suiteConfig);
482
548
  const personaYamlPaths = await discoverSuitePersonaYamls(suiteConfig);
483
549
  const results = [];
484
550
  for (const yamlPath of personaYamlPaths) {
@@ -594,6 +660,6 @@ function escapeRegExp(str) {
594
660
  var _pkgRequire = createRequire(import.meta.url);
595
661
  var VERSION = _pkgRequire("../package.json").version;
596
662
 
597
- export { DEFAULT_FRONTMATTER_CLAUDE_CODE, DEFAULT_FRONTMATTER_DEEP_AGENTS, DEFAULT_FRONTMATTER_VSCODE, TARGET_CLAUDE_CODE, TARGET_DEEP_AGENTS, TARGET_VSCODE, TargetRegistry, VERSION, build, buildPersona, buildSuite, collapseBlankLines, defaultRegistry, discoverPersonaYamls, ensureBlankLineBeforeHeadings, escapeRegExp, loadContent, loadMetadata, loadPartials, normalizeNewlines, renderFrontmatter, resolveConditionals, resolveFrontmatterTemplate, resolvePartials, resolveVariables, runBuildContext, runPostRender, runSuiteInit, runValidate, serializeTools, serializeToolsList, validateFileName, validateStrictMarkers };
663
+ export { DEFAULT_FRONTMATTER_CLAUDE_CODE, DEFAULT_FRONTMATTER_DEEP_AGENTS, DEFAULT_FRONTMATTER_VSCODE, TARGET_CLAUDE_CODE, TARGET_DEEP_AGENTS, TARGET_VSCODE, TargetRegistry, VERSION, build, buildPersona, buildSuite, collapseBlankLines, defaultRegistry, discoverPersonaYamls, ensureBlankLineBeforeHeadings, escapeRegExp, loadContent, loadMetadata, loadPartials, normalizeNewlines, renderFrontmatter, resolveConditionals, resolveFrontmatterTemplate, resolvePartials, resolveVariables, runBuildContext, runPartials, runPersonaPartials, runPostRender, runSuiteInit, runValidate, serializeTools, serializeToolsList, validateFileName, validateStrictMarkers };
598
664
  //# sourceMappingURL=index.js.map
599
665
  //# sourceMappingURL=index.js.map