@pikku/cli 0.12.92 → 0.12.96

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.
Files changed (196) hide show
  1. package/console-app/assets/{index-DDpIMCpy.js → index-CeQodYfg.js} +151 -149
  2. package/console-app/assets/{index-D0HG8q0B.css → index-CyYHTGWZ.css} +1 -1
  3. package/console-app/index.html +2 -2
  4. package/dist/.pikku/agent/pikku-agent-types.gen.d.ts +1 -1
  5. package/dist/.pikku/channel/pikku-channel-types.gen.d.ts +1 -1
  6. package/dist/.pikku/channel/pikku-channel-types.gen.js +1 -1
  7. package/dist/.pikku/cli/pikku-cli-channel.d.ts +13 -15
  8. package/dist/.pikku/cli/pikku-cli-channel.js +72 -3
  9. package/dist/.pikku/cli/pikku-cli-client.gen.d.ts +14 -4
  10. package/dist/.pikku/cli/pikku-cli-client.gen.js +20 -10
  11. package/dist/.pikku/cli/pikku-cli-contracts-meta.gen.d.ts +1 -1
  12. package/dist/.pikku/cli/pikku-cli-contracts-meta.gen.js +1 -1
  13. package/dist/.pikku/cli/pikku-cli-types.gen.d.ts +1 -1
  14. package/dist/.pikku/cli/pikku-cli-types.gen.js +1 -1
  15. package/dist/.pikku/cli/pikku-cli-wirings-meta.gen.js +1 -1
  16. package/dist/.pikku/cli/pikku-cli-wirings-meta.gen.json +156 -2
  17. package/dist/.pikku/cli/pikku-cli-wirings.gen.d.ts +1 -1
  18. package/dist/.pikku/cli/pikku-cli-wirings.gen.js +1 -1
  19. package/dist/.pikku/cli/pikku-cli.gen.d.ts +1 -1
  20. package/dist/.pikku/cli/pikku-cli.gen.js +1 -1
  21. package/dist/.pikku/console/pikku-node-types.gen.d.ts +1 -1
  22. package/dist/.pikku/function/pikku-function-types.gen.d.ts +55 -20
  23. package/dist/.pikku/function/pikku-function-types.gen.js +27 -1
  24. package/dist/.pikku/function/pikku-functions-meta.gen.js +1 -1
  25. package/dist/.pikku/function/pikku-functions-meta.gen.json +176 -13
  26. package/dist/.pikku/function/pikku-functions.gen.js +11 -1
  27. package/dist/.pikku/http/pikku-http-types.gen.d.ts +1 -1
  28. package/dist/.pikku/http/pikku-http-types.gen.js +1 -1
  29. package/dist/.pikku/mcp/pikku-mcp-types.gen.d.ts +1 -1
  30. package/dist/.pikku/mcp/pikku-mcp-types.gen.js +1 -1
  31. package/dist/.pikku/pikku-bootstrap-scenarios.gen.d.ts +1 -1
  32. package/dist/.pikku/pikku-bootstrap-scenarios.gen.js +1 -1
  33. package/dist/.pikku/pikku-bootstrap.gen.d.ts +1 -1
  34. package/dist/.pikku/pikku-bootstrap.gen.js +1 -1
  35. package/dist/.pikku/pikku-meta-service.gen.d.ts +1 -1
  36. package/dist/.pikku/pikku-meta-service.gen.js +1 -1
  37. package/dist/.pikku/pikku-services.gen.d.ts +1 -1
  38. package/dist/.pikku/pikku-types.gen.d.ts +1 -1
  39. package/dist/.pikku/pikku-types.gen.js +1 -1
  40. package/dist/.pikku/queue/pikku-queue-types.gen.d.ts +1 -1
  41. package/dist/.pikku/queue/pikku-queue-types.gen.js +1 -1
  42. package/dist/.pikku/queue/pikku-queue-workers-wirings-meta.gen.js +1 -1
  43. package/dist/.pikku/queue/pikku-queue-workers-wirings.gen.d.ts +1 -1
  44. package/dist/.pikku/queue/pikku-queue-workers-wirings.gen.js +1 -1
  45. package/dist/.pikku/rpc/pikku-rpc-wirings-meta.internal.gen.js +1 -1
  46. package/dist/.pikku/rpc/pikku-rpc-wirings-meta.internal.gen.json +9 -0
  47. package/dist/.pikku/scenarios/pikku-scenario-functions-meta.gen.js +1 -1
  48. package/dist/.pikku/scenarios/pikku-scenario-functions.gen.d.ts +1 -1
  49. package/dist/.pikku/scenarios/pikku-scenario-wirings-meta.gen.js +1 -1
  50. package/dist/.pikku/scenarios/pikku-scenario-wirings.gen.d.ts +1 -1
  51. package/dist/.pikku/scenarios/schemas/register.gen.d.ts +1 -1
  52. package/dist/.pikku/scenarios/schemas/register.gen.js +1 -1
  53. package/dist/.pikku/scheduler/pikku-scheduler-types.gen.d.ts +1 -1
  54. package/dist/.pikku/scheduler/pikku-scheduler-types.gen.js +1 -1
  55. package/dist/.pikku/schemas/register.gen.js +21 -3
  56. package/dist/.pikku/schemas/schemas/KnowledgeIndexInput.schema.json +1 -0
  57. package/dist/.pikku/schemas/schemas/KnowledgeIndexOutput.schema.json +1 -0
  58. package/dist/.pikku/schemas/schemas/KnowledgeValidateInput.schema.json +1 -0
  59. package/dist/.pikku/schemas/schemas/KnowledgeValidateOutput.schema.json +1 -0
  60. package/dist/.pikku/schemas/schemas/PersonaRunInput.schema.json +1 -0
  61. package/dist/.pikku/schemas/schemas/PersonaSyncInput.schema.json +1 -0
  62. package/dist/.pikku/schemas/schemas/PikkuCLIConfig.schema.json +1 -1
  63. package/dist/.pikku/schemas/schemas/PikkuPersonasInput.schema.json +1 -0
  64. package/dist/.pikku/schemas/schemas/PikkuRolesInput.schema.json +1 -0
  65. package/dist/.pikku/schemas/schemas/RolesAuditInput.schema.json +1 -0
  66. package/dist/.pikku/schemas/schemas/RolesPruneInput.schema.json +1 -0
  67. package/dist/.pikku/scopes/pikku-personas-meta.gen.json +1 -0
  68. package/dist/.pikku/scopes/pikku-personas.gen.d.ts +36 -0
  69. package/dist/.pikku/scopes/pikku-personas.gen.js +17 -0
  70. package/dist/.pikku/scopes/pikku-roles-meta.gen.json +1 -0
  71. package/dist/.pikku/scopes/pikku-roles.gen.d.ts +30 -0
  72. package/dist/.pikku/scopes/pikku-roles.gen.js +18 -0
  73. package/dist/.pikku/scopes/pikku-scope-types.gen.d.ts +4 -2
  74. package/dist/.pikku/scopes/pikku-scope-types.gen.js +3 -2
  75. package/dist/.pikku/scopes/pikku-scopes.gen.d.ts +1 -1
  76. package/dist/.pikku/secrets/pikku-secret-types.gen.d.ts +2 -2
  77. package/dist/.pikku/secrets/pikku-secret-types.gen.js +2 -2
  78. package/dist/.pikku/secrets/pikku-secrets.gen.d.ts +1 -1
  79. package/dist/.pikku/secrets/pikku-secrets.gen.js +1 -1
  80. package/dist/.pikku/trigger/pikku-trigger-types.gen.d.ts +1 -1
  81. package/dist/.pikku/trigger/pikku-trigger-types.gen.js +1 -1
  82. package/dist/.pikku/variables/pikku-variable-types.gen.d.ts +2 -2
  83. package/dist/.pikku/variables/pikku-variable-types.gen.js +2 -2
  84. package/dist/.pikku/variables/pikku-variables.gen.d.ts +1 -1
  85. package/dist/.pikku/variables/pikku-variables.gen.js +1 -1
  86. package/dist/.pikku/workflow/meta/allWorkflow.gen.json +27 -3
  87. package/dist/.pikku/workflow/pikku-personas.gen.d.ts +15 -0
  88. package/dist/.pikku/workflow/pikku-personas.gen.js +17 -0
  89. package/dist/.pikku/workflow/pikku-workflow-types.gen.d.ts +96 -19
  90. package/dist/.pikku/workflow/pikku-workflow-types.gen.js +22 -2
  91. package/dist/.pikku/workflow/pikku-workflow-wirings-meta.gen.js +1 -1
  92. package/dist/.pikku/workflow/pikku-workflow-wirings.gen.js +1 -1
  93. package/dist/bin/pikku-bin.mjs +2 -2
  94. package/dist/bin/pikku.js +0 -0
  95. package/dist/src/cli.wiring.js +124 -2
  96. package/dist/src/deploy/analyzer/analyzer.js +14 -0
  97. package/dist/src/deploy/analyzer/manifest.d.ts +11 -0
  98. package/dist/src/fabric/functions/validate.function.js +20 -11
  99. package/dist/src/functions/commands/bootstrap.js +3 -0
  100. package/dist/src/functions/commands/dev-ai-runner.d.ts +16 -4
  101. package/dist/src/functions/commands/dev-ai-runner.js +63 -29
  102. package/dist/src/functions/commands/dev.js +9 -4
  103. package/dist/src/functions/commands/enable.js +15 -19
  104. package/dist/src/functions/commands/{scenario-environment.d.ts → environment.d.ts} +9 -6
  105. package/dist/src/functions/commands/{scenario-environment.js → environment.js} +5 -4
  106. package/dist/src/functions/commands/knowledge-index.d.ts +30 -0
  107. package/dist/src/functions/commands/knowledge-index.js +11 -0
  108. package/dist/src/functions/commands/knowledge-validate.d.ts +33 -0
  109. package/dist/src/functions/commands/knowledge-validate.js +11 -0
  110. package/dist/src/functions/commands/new-addon.d.ts +18 -0
  111. package/dist/src/functions/commands/new-addon.js +52 -36
  112. package/dist/src/functions/commands/persona-sync.d.ts +33 -0
  113. package/dist/src/functions/commands/persona-sync.js +151 -0
  114. package/dist/src/functions/commands/persona.d.ts +58 -0
  115. package/dist/src/functions/commands/persona.js +217 -0
  116. package/dist/src/functions/commands/roles-audit.d.ts +1 -0
  117. package/dist/src/functions/commands/roles-audit.js +37 -0
  118. package/dist/src/functions/commands/roles-prune.d.ts +16 -0
  119. package/dist/src/functions/commands/roles-prune.js +52 -0
  120. package/dist/src/functions/commands/roles-shared.d.ts +40 -0
  121. package/dist/src/functions/commands/roles-shared.js +77 -0
  122. package/dist/src/functions/commands/scenario-browser.d.ts +3 -3
  123. package/dist/src/functions/commands/scenario-browser.js +1 -1
  124. package/dist/src/functions/commands/scenario-ladder.d.ts +44 -2
  125. package/dist/src/functions/commands/scenario-ladder.js +51 -2
  126. package/dist/src/functions/commands/scenario.d.ts +7 -0
  127. package/dist/src/functions/commands/scenario.js +79 -17
  128. package/dist/src/functions/commands/scopes-shared.d.ts +8 -0
  129. package/dist/src/functions/commands/scopes-shared.js +1 -1
  130. package/dist/src/functions/commands/serve.js +7 -3
  131. package/dist/src/functions/commands/virtual-user-formatter.d.ts +30 -0
  132. package/dist/src/functions/commands/virtual-user-formatter.js +65 -0
  133. package/dist/src/functions/db/local-db.js +38 -1
  134. package/dist/src/functions/knowledge/render.d.ts +3 -0
  135. package/dist/src/functions/knowledge/render.js +66 -0
  136. package/dist/src/functions/knowledge/schemas.d.ts +51 -0
  137. package/dist/src/functions/knowledge/schemas.js +25 -0
  138. package/dist/src/functions/validate/workspace-validate.js +1 -1
  139. package/dist/src/functions/wirings/ai-agent/pikku-command-public-agent.js +2 -1
  140. package/dist/src/functions/wirings/ai-agent/serialize-public-agent.js +1 -0
  141. package/dist/src/functions/wirings/auth/serialize-auth-gen.d.ts +2 -2
  142. package/dist/src/functions/wirings/auth/serialize-auth-gen.js +8 -8
  143. package/dist/src/functions/wirings/cli/serialize-channel-cli-client.d.ts +7 -3
  144. package/dist/src/functions/wirings/cli/serialize-channel-cli-client.js +32 -18
  145. package/dist/src/functions/wirings/cli/serialize-channel-cli.js +63 -6
  146. package/dist/src/functions/wirings/console/serialize-console-functions.js +4 -7
  147. package/dist/src/functions/wirings/credentials/serialize-credentials-types.js +8 -1
  148. package/dist/src/functions/wirings/functions/serialize-function-types.js +88 -28
  149. package/dist/src/functions/wirings/middleware/serialize-middleware-imports.js +25 -9
  150. package/dist/src/functions/wirings/package/serialize-package-types.js +5 -3
  151. package/dist/src/functions/wirings/personas/pikku-command-personas.d.ts +7 -0
  152. package/dist/src/functions/wirings/personas/pikku-command-personas.js +43 -0
  153. package/dist/src/functions/wirings/personas/serialize-personas-types.d.ts +18 -0
  154. package/dist/src/functions/wirings/personas/serialize-personas-types.js +76 -0
  155. package/dist/src/functions/wirings/realtime/pikku-command-events-scaffold.js +2 -1
  156. package/dist/src/functions/wirings/roles/pikku-command-roles.d.ts +7 -0
  157. package/dist/src/functions/wirings/roles/pikku-command-roles.js +33 -0
  158. package/dist/src/functions/wirings/roles/serialize-roles-types.d.ts +14 -0
  159. package/dist/src/functions/wirings/roles/serialize-roles-types.js +55 -0
  160. package/dist/src/functions/wirings/rpc/pikku-command-public-rpc.js +2 -1
  161. package/dist/src/functions/wirings/scenarios/register-scenario-instrumentation.d.ts +3 -3
  162. package/dist/src/functions/wirings/scenarios/register-scenario-instrumentation.js +3 -3
  163. package/dist/src/functions/wirings/scopes/serialize-scopes-types.d.ts +1 -1
  164. package/dist/src/functions/wirings/scopes/serialize-scopes-types.js +1 -1
  165. package/dist/src/functions/wirings/secrets/serialize-secrets-types.js +10 -3
  166. package/dist/src/functions/wirings/user-admin/pikku-command-user-admin-functions.js +2 -1
  167. package/dist/src/functions/wirings/user-admin/serialize-user-admin-functions.js +2 -2
  168. package/dist/src/functions/wirings/variables/serialize-variables-types.js +8 -1
  169. package/dist/src/functions/wirings/workflow/pikku-command-workflow-routes.js +2 -1
  170. package/dist/src/functions/wirings/workflow/pikku-command-workflow.js +13 -13
  171. package/dist/src/functions/wirings/workflow/serialize-personas.d.ts +10 -0
  172. package/dist/src/functions/wirings/workflow/serialize-personas.js +46 -0
  173. package/dist/src/functions/wirings/workflow/serialize-scenario-step-map.js +11 -2
  174. package/dist/src/functions/wirings/workflow/serialize-workflow-types.js +165 -29
  175. package/dist/src/functions/workflows/all.workflow.js +7 -1
  176. package/dist/src/server/content-signing-jwt.d.ts +13 -0
  177. package/dist/src/server/content-signing-jwt.js +51 -0
  178. package/dist/src/server/dev-server-runner.interface.d.ts +10 -2
  179. package/dist/src/server/node-server-runner.d.ts +2 -2
  180. package/dist/src/server/node-server-runner.js +2 -1
  181. package/dist/src/services.d.ts +3 -1
  182. package/dist/src/services.js +2 -1
  183. package/dist/src/utils/pikku-cli-config.js +45 -2
  184. package/dist/src/utils/resolve-personas.d.ts +32 -0
  185. package/dist/src/utils/resolve-personas.js +46 -0
  186. package/dist/src/utils/resolve-scaffold-feature.d.ts +19 -0
  187. package/dist/src/utils/resolve-scaffold-feature.js +49 -0
  188. package/dist/tsconfig.tsbuildinfo +1 -1
  189. package/package.json +17 -13
  190. package/dist/.pikku/schemas/schemas/ScenarioRunInput.schema.json +0 -1
  191. package/dist/.pikku/workflow/pikku-scenario-actors.gen.d.ts +0 -19
  192. package/dist/.pikku/workflow/pikku-scenario-actors.gen.js +0 -17
  193. package/dist/src/functions/wirings/workflow/serialize-scenario-actors.d.ts +0 -2
  194. package/dist/src/functions/wirings/workflow/serialize-scenario-actors.js +0 -40
  195. package/dist/src/utils/resolve-scenario-actors.d.ts +0 -23
  196. package/dist/src/utils/resolve-scenario-actors.js +0 -74
@@ -0,0 +1,217 @@
1
+ import { resolve, dirname } from 'node:path';
2
+ import { mkdirSync, writeFileSync } from 'node:fs';
3
+ import { pikkuSessionlessFunc } from '#pikku';
4
+ import { createHttpPersonas, personaEnvironmentRefusal, roleMismatchMessage, verifyPersonaRoles, } from '@pikku/core/persona';
5
+ import { flattenSystemRoleDefinitions } from '@pikku/core/role';
6
+ import { personaVirtualUserTarget, catalogueClassification, deriveCatalogue, deriveIntents, DISPOSITIONS, reachableAgents, runVirtualUser, } from '@pikku/core/virtual-user';
7
+ import { personaScopes, resolvePersonas } from '../../utils/resolve-personas.js';
8
+ import { resolveEnvironment } from './environment.js';
9
+ import { createDevAIAgentRunner } from './dev-ai-runner.js';
10
+ import { formatVirtualUserReport } from './virtual-user-formatter.js';
11
+ const dispositionNames = Object.keys(DISPOSITIONS);
12
+ /**
13
+ * What a declaration overrode, in the order it was written.
14
+ *
15
+ * Listed rather than summarised as "tuned": the whole reason to override a dial
16
+ * is that the stock profile was wrong for this product, and which one you moved
17
+ * is the interesting part.
18
+ */
19
+ const describeTuning = (tuning) => Object.entries(tuning)
20
+ .map(([dial, value]) => dial === 'moves' && value && typeof value === 'object'
21
+ ? Object.entries(value)
22
+ .map(([move, weight]) => `${move} ${weight}`)
23
+ .join(', ')
24
+ : `${dial} ${dial === 'instructions' ? '+' : value}`)
25
+ .join(', ');
26
+ /**
27
+ * Run one declared persona against a real stage as a virtual user.
28
+ *
29
+ * Everything it needs is already in the project: the catalogue is the function
30
+ * meta, the intents are the scenarios' own prose, the identity is the persona
31
+ * signing in over the app's real auth, and the scopes come from the roles they
32
+ * declared. The only new input is which person to be — everything else is a
33
+ * flag for narrowing or replaying a run.
34
+ *
35
+ * This is deliberately not a test runner. It asserts nothing, and a green run
36
+ * proves nothing — what it produces is findings, and the absence of them is
37
+ * only ever "not this time, not with this seed".
38
+ */
39
+ export const personaRun = pikkuSessionlessFunc({
40
+ func: async ({ logger, config, getInspectorState, variables }, { environment, persona: personaId, disposition: dispositionFlag, goals, steps, mutations, duration, seed, model, apiUrl, allowApproval, skipRoleCheck, out, }) => {
41
+ const state = await getInspectorState(true, false, false, true);
42
+ const personas = resolvePersonas(state.personas?.definitions ?? [], config.scenarios?.emailDomain);
43
+ const known = Object.keys(personas);
44
+ const persona = personas[personaId];
45
+ if (!persona) {
46
+ throw new Error(`No persona '${personaId}'.${known.length
47
+ ? ` Declared: ${known.join(', ')}`
48
+ : ' None are declared — add a definePersonas({ … }) call.'}`);
49
+ }
50
+ // A provider login needs a human at a consent screen, and someone declared
51
+ // `runnable: false` exists to be acted upon. Refused here rather than
52
+ // failing partway through a sign-in that was never going to work.
53
+ if (!persona.runnable) {
54
+ throw new Error(`Persona '${personaId}' is not runnable — ${persona.account?.provider
55
+ ? `their login is a '${persona.account.provider}' account, and driving its consent screen needs a person`
56
+ : 'they are declared `runnable: false`, to be acted upon rather than to act'}.`);
57
+ }
58
+ const declared = dispositionFlag ?? persona.disposition ?? 'realistic';
59
+ if (!dispositionNames.includes(declared)) {
60
+ throw new Error(`Unknown disposition '${declared}'. One of: ${dispositionNames.join(', ')}`);
61
+ }
62
+ // Narrowed by the guard above; `dispositionNames` is the runtime list and
63
+ // does not carry the union through `includes`.
64
+ const disposition = declared;
65
+ const environments = config.environments ?? {};
66
+ const env = resolveEnvironment({
67
+ environment,
68
+ environments,
69
+ apiUrl,
70
+ });
71
+ // Checked against the *effective* disposition, so `--disposition` cannot
72
+ // turn an accountable persona adversarial and point it at production. The
73
+ // build check already looked at the declaration; this one exists because
74
+ // the declaration is not what is running — the deployed artifact is.
75
+ const refusal = personaEnvironmentRefusal(personaId, { ...persona, disposition }, environment, environments);
76
+ if (refusal) {
77
+ throw new Error(refusal);
78
+ }
79
+ const secret = await variables.get('SCENARIO_ACTOR_SECRET');
80
+ if (!secret) {
81
+ throw new Error('SCENARIO_ACTOR_SECRET is not set — a virtual user cannot sign in. ' +
82
+ 'Export it in the environment running this command (never put it in pikku.config.json).');
83
+ }
84
+ const resolvedModel = model ?? config.scenarios?.model;
85
+ if (!resolvedModel) {
86
+ throw new Error('No model to think with — pass --model, or set scenarios.model in pikku.config.json.');
87
+ }
88
+ const aiAgentRunner = await createDevAIAgentRunner({
89
+ logger,
90
+ projectRoot: config.rootDir,
91
+ variables,
92
+ });
93
+ if (!aiAgentRunner) {
94
+ throw new Error('No AI provider is configured. Set OPENAI_BASE_URL + OPENAI_API_KEY (or LITELLM_PROXY_URL + LITELLM_API_KEY).');
95
+ }
96
+ const functionsMeta = state.functions?.meta ?? {};
97
+ const catalogue = deriveCatalogue(functionsMeta, (state.schemas ?? {}));
98
+ if (catalogue.length === 0) {
99
+ throw new Error('This project exposes no RPCs, so there is nothing for a virtual user to do.');
100
+ }
101
+ const intents = deriveIntents(state.workflows?.meta ?? {}, functionsMeta);
102
+ // Roles are what a persona declares; scopes are what a function checks.
103
+ // Expanded once, here, from the same definitions the seed grants from.
104
+ const roleScopes = {};
105
+ for (const role of flattenSystemRoleDefinitions(state.systemRoles?.definitions ?? [])) {
106
+ roleScopes[role.name] = role.scopes;
107
+ }
108
+ const scopes = personaScopes(persona, roleScopes);
109
+ // Gated by the same scopes as the RPCs, because an agent is reached rather
110
+ // than declared: `CoreAIAgent.scopes` is checked against the session, so a
111
+ // persona finds the specialists its roles unlock and no others.
112
+ const agents = reachableAgents(state.agents?.agentsMeta ?? {}, scopes);
113
+ const signedIn = createHttpPersonas({
114
+ apiUrl: env.apiUrl,
115
+ secret,
116
+ personas,
117
+ signInPath: env.signInPath,
118
+ rpcPath: env.rpcPath,
119
+ model: resolvedModel,
120
+ });
121
+ // Before the first step, not after: findings from a persona whose roles
122
+ // drifted are about the seed, and reading them as product bugs is how a
123
+ // whole run gets thrown away. `--skip-role-check` exists for a stage whose
124
+ // auth reports roles somewhere this cannot read.
125
+ if (!skipRoleCheck) {
126
+ const actual = await signedIn[personaId].sessionRoles();
127
+ if (actual === null) {
128
+ logger.warn(`Could not read '${personaId}' roles back from ${env.apiUrl} — running unverified. ` +
129
+ `A stage that does not report roles cannot tell a permissions finding from seed drift.`);
130
+ }
131
+ else {
132
+ const message = roleMismatchMessage(verifyPersonaRoles(personaId, persona.roles, actual));
133
+ if (message) {
134
+ throw new Error(message);
135
+ }
136
+ }
137
+ }
138
+ const flagGoals = goals?.split(',').map((goal) => goal.trim());
139
+ const result = await runVirtualUser({
140
+ persona,
141
+ personaId,
142
+ disposition,
143
+ // Tuning belongs to the declared person, so `--disposition` drops it: you
144
+ // asked to run them differently, not to bend their dials to another shape.
145
+ ...(dispositionFlag ? {} : { tuning: persona.tuning }),
146
+ catalogue,
147
+ intents,
148
+ goals: [...persona.goals, ...(flagGoals ?? [])],
149
+ scopes,
150
+ fixtures: persona.fixtures,
151
+ // Both halves, or `talkTo` is wired up and never advertised: the target
152
+ // decides whether a name is callable, the instructions are what tell the
153
+ // persona the assistant exists at all.
154
+ agents,
155
+ target: personaVirtualUserTarget(signedIn[personaId], {
156
+ model: resolvedModel,
157
+ agents: agents.map((agent) => agent.name),
158
+ }),
159
+ llm: (params) => aiAgentRunner.run(params),
160
+ model: resolvedModel,
161
+ seed: seed ? Number(seed) : undefined,
162
+ allowApprovalRequired: allowApproval ?? false,
163
+ budget: {
164
+ steps: steps ? Number(steps) : undefined,
165
+ mutations: mutations ? Number(mutations) : undefined,
166
+ duration,
167
+ },
168
+ });
169
+ for (const { level, text } of formatVirtualUserReport(result, {
170
+ persona: `${persona.name} (${personaId})`,
171
+ disposition,
172
+ environment,
173
+ apiUrl: env.apiUrl,
174
+ catalogue: catalogueClassification(catalogue),
175
+ })) {
176
+ logger[level](text);
177
+ }
178
+ if (out) {
179
+ const path = resolve(config.rootDir, out);
180
+ mkdirSync(dirname(path), { recursive: true });
181
+ writeFileSync(path, JSON.stringify(result, null, 2));
182
+ logger.info(`Wrote the full run to ${path}`);
183
+ }
184
+ // A finding is the whole reason this ran, so it has to be able to fail a
185
+ // pipeline. Nothing else here does: giving up on an intent is a user being
186
+ // a user, not a defect.
187
+ if (result.findings.length > 0) {
188
+ process.exitCode = 1;
189
+ }
190
+ },
191
+ });
192
+ /** Who this app is for, and what each of them wants, without running anything. */
193
+ export const personaList = pikkuSessionlessFunc({
194
+ func: async ({ logger, config, getInspectorState }) => {
195
+ const state = await getInspectorState(true, false, false, true);
196
+ const personas = Object.values(resolvePersonas(state.personas?.definitions ?? [], config.scenarios?.emailDomain));
197
+ if (personas.length === 0) {
198
+ logger.info('No personas declared. Add a definePersonas({ susan: { name, roles, goals } }) call.');
199
+ return;
200
+ }
201
+ for (const persona of personas) {
202
+ logger.info(`${persona.id} — ${persona.name}${persona.jobTitle ? `, ${persona.jobTitle}` : ''}`);
203
+ logger.info(` ${persona.email}${persona.roles.length
204
+ ? `, roles: ${persona.roles.join(', ')}`
205
+ : ', no roles'}${persona.runnable ? '' : ' (not runnable)'}`);
206
+ if (persona.disposition || persona.tuning) {
207
+ logger.info(` ${persona.disposition ?? 'realistic'}${persona.tuning ? ` (tuned: ${describeTuning(persona.tuning)})` : ''}`);
208
+ }
209
+ if (persona.description) {
210
+ logger.info(` ${persona.description}`);
211
+ }
212
+ for (const goal of persona.goals) {
213
+ logger.info(` · ${goal}`);
214
+ }
215
+ }
216
+ },
217
+ });
@@ -0,0 +1 @@
1
+ export declare const rolesAudit: import("#pikku").PikkuFunctionConfig<{}, void, "rpc" | "session", import("#pikku").PikkuFunctionSessionless<{}, void, "rpc" | "session", import("#pikku").WiredServices> | import("#pikku").PikkuFunction<{}, void, "rpc" | "session", import("#pikku").WiredServices>, undefined, undefined>;
@@ -0,0 +1,37 @@
1
+ import { pikkuSessionlessFunc } from '#pikku';
2
+ import { loadDeclaredRoles, openScopeServiceForRoles, reportStaleRoles, } from './roles-shared.js';
3
+ export const rolesAudit = pikkuSessionlessFunc({
4
+ remote: true,
5
+ func: async ({ logger, config }) => {
6
+ const declared = await loadDeclaredRoles(config.rolesMetaJsonFile, logger);
7
+ if (!declared) {
8
+ throw new Error('role metadata not found');
9
+ }
10
+ const opened = await openScopeServiceForRoles({ config, logger }, declared, 'pikku roles audit');
11
+ if (!opened) {
12
+ return;
13
+ }
14
+ try {
15
+ const stale = await opened.service.findStaleSystemRoles();
16
+ logger.info(`Role audit: ${declared.length} system role(s) declared in code`);
17
+ if (stale.length === 0) {
18
+ logger.info(' every system role in the database is still declared');
19
+ return;
20
+ }
21
+ logger.info('');
22
+ logger.info(`${stale.length} system role(s) no longer declared in code:`);
23
+ reportStaleRoles(stale, logger);
24
+ const held = stale.filter((s) => s.users > 0);
25
+ logger.info('');
26
+ if (held.length > 0) {
27
+ logger.warn(`${held.length} undeclared role(s) are still held by somebody. ` +
28
+ `They are inert — nothing offers them for new grants — but the ` +
29
+ `existing grants persist, and so do whatever scopes the role still carries.`);
30
+ }
31
+ logger.info('Run `pikku roles prune --yes` to remove them.');
32
+ }
33
+ finally {
34
+ await opened.destroy();
35
+ }
36
+ },
37
+ });
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Removes system roles that are no longer declared in code, cascading them out
3
+ * of every user grant that holds them.
4
+ *
5
+ * This revokes access, so it never runs implicitly — not at boot, not as part
6
+ * of `pikku all`. Without `--yes` it only reports the blast radius. That is the
7
+ * whole point of the additive sync: deleting a `defineSystemRole` declaration
8
+ * leaves people holding the role until somebody decides otherwise here.
9
+ */
10
+ export declare const rolesPrune: import("#pikku").PikkuFunctionConfig<{
11
+ yes?: boolean;
12
+ }, void, "rpc" | "session", import("#pikku").PikkuFunctionSessionless<{
13
+ yes?: boolean;
14
+ }, void, "rpc" | "session", import("#pikku").WiredServices> | import("#pikku").PikkuFunction<{
15
+ yes?: boolean;
16
+ }, void, "rpc" | "session", import("#pikku").WiredServices>, undefined, undefined>;
@@ -0,0 +1,52 @@
1
+ import { pikkuSessionlessFunc } from '#pikku';
2
+ import { loadDeclaredRoles, openScopeServiceForRoles, reportStaleRoles, } from './roles-shared.js';
3
+ /**
4
+ * Removes system roles that are no longer declared in code, cascading them out
5
+ * of every user grant that holds them.
6
+ *
7
+ * This revokes access, so it never runs implicitly — not at boot, not as part
8
+ * of `pikku all`. Without `--yes` it only reports the blast radius. That is the
9
+ * whole point of the additive sync: deleting a `defineSystemRole` declaration
10
+ * leaves people holding the role until somebody decides otherwise here.
11
+ */
12
+ export const rolesPrune = pikkuSessionlessFunc({
13
+ remote: true,
14
+ func: async ({ logger, config }, { yes }) => {
15
+ const declared = await loadDeclaredRoles(config.rolesMetaJsonFile, logger);
16
+ if (!declared) {
17
+ throw new Error('role metadata not found');
18
+ }
19
+ const opened = await openScopeServiceForRoles({ config, logger }, declared, 'pikku roles prune');
20
+ if (!opened) {
21
+ return;
22
+ }
23
+ try {
24
+ const stale = await opened.service.findStaleSystemRoles();
25
+ if (stale.length === 0) {
26
+ logger.info('roles prune: nothing to prune');
27
+ return;
28
+ }
29
+ logger.info(`${stale.length} system role(s) no longer declared in code:`);
30
+ reportStaleRoles(stale, logger);
31
+ const held = stale.filter((s) => s.users > 0);
32
+ const affectedUsers = stale.reduce((sum, s) => sum + s.users, 0);
33
+ if (!yes) {
34
+ logger.info('');
35
+ if (held.length > 0) {
36
+ logger.warn(`Pruning would revoke ${held.length} role(s) from ${affectedUsers} user grant(s).`);
37
+ }
38
+ logger.info('Re-run with --yes to remove them.');
39
+ return;
40
+ }
41
+ const pruned = await opened.service.pruneSystemRoles();
42
+ logger.info('');
43
+ logger.info(`roles prune: removed ${pruned.length} system role(s)`);
44
+ if (held.length > 0) {
45
+ logger.warn(`Revoked from ${affectedUsers} user grant(s). They lose the role's scopes on their next request.`);
46
+ }
47
+ }
48
+ finally {
49
+ await opened.destroy();
50
+ }
51
+ },
52
+ });
@@ -0,0 +1,40 @@
1
+ import type { SystemRole } from '@pikku/core/role';
2
+ import { openScopeService } from './scopes-shared.js';
3
+ import type { OpenedScopeService } from './scopes-shared.js';
4
+ type Logger = {
5
+ info: (msg: string) => void;
6
+ warn: (msg: string) => void;
7
+ error: (msg: string) => void;
8
+ };
9
+ /**
10
+ * Reads the generated role metadata and flattens it to the declared role set.
11
+ *
12
+ * Returns `null` — never an empty array — when the file cannot be read, for the
13
+ * same reason `loadDeclaredScopes` does: `prune` removes everything not in this
14
+ * set, so "nothing is declared" and "we failed to find out" have to stay
15
+ * distinguishable or a missing file wipes every system role in the database.
16
+ */
17
+ export declare const loadDeclaredRoles: (rolesMetaJsonFile: string | undefined, logger: Logger) => Promise<SystemRole[] | null>;
18
+ /**
19
+ * Opens a ScopeService and brings both the scope and the role `declared` marks
20
+ * up to date with the code.
21
+ *
22
+ * Scopes are synced first and not as a courtesy: `pikku_role_scopes` has a
23
+ * foreign key onto `pikku_scopes`, so a role granting a freshly-declared scope
24
+ * cannot be written until that scope's row exists. Both syncs are additive, so
25
+ * doing them inside a read-only command is safe.
26
+ */
27
+ export declare const openScopeServiceForRoles: (args: Parameters<typeof openScopeService>[0] & {
28
+ config: {
29
+ scopesMetaJsonFile: string;
30
+ };
31
+ }, roles: SystemRole[], command: string) => Promise<OpenedScopeService | null>;
32
+ /**
33
+ * Renders undeclared system roles and how many people still hold each — the
34
+ * blast radius, shown before anything is deleted.
35
+ */
36
+ export declare const reportStaleRoles: (stale: Array<{
37
+ role: string;
38
+ users: number;
39
+ }>, logger: Logger) => void;
40
+ export {};
@@ -0,0 +1,77 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { flattenSystemRoleDefinitions } from '@pikku/core/role';
3
+ import { loadDeclaredScopes, openScopeService } from './scopes-shared.js';
4
+ const isRoleDefinitionsMeta = (value) => typeof value === 'object' &&
5
+ value !== null &&
6
+ !Array.isArray(value) &&
7
+ Object.values(value).every((def) => typeof def === 'object' &&
8
+ def !== null &&
9
+ !Array.isArray(def) &&
10
+ typeof def.name === 'string' &&
11
+ Array.isArray(def.scopes));
12
+ /**
13
+ * Reads the generated role metadata and flattens it to the declared role set.
14
+ *
15
+ * Returns `null` — never an empty array — when the file cannot be read, for the
16
+ * same reason `loadDeclaredScopes` does: `prune` removes everything not in this
17
+ * set, so "nothing is declared" and "we failed to find out" have to stay
18
+ * distinguishable or a missing file wipes every system role in the database.
19
+ */
20
+ export const loadDeclaredRoles = async (rolesMetaJsonFile, logger) => {
21
+ if (!rolesMetaJsonFile) {
22
+ logger.error('pikku roles: no role metadata path is configured.\n' +
23
+ ' Run `pikku all` to generate it.');
24
+ return null;
25
+ }
26
+ let parsed;
27
+ try {
28
+ parsed = JSON.parse(await readFile(rolesMetaJsonFile, 'utf8'));
29
+ }
30
+ catch {
31
+ logger.error(`pikku roles: no role metadata at ${rolesMetaJsonFile}.\n` +
32
+ ` Run \`pikku all\` to generate it.`);
33
+ return null;
34
+ }
35
+ if (!isRoleDefinitionsMeta(parsed)) {
36
+ logger.error(`pikku roles: the role metadata at ${rolesMetaJsonFile} is malformed.\n` +
37
+ ` Run \`pikku all\` to regenerate it.`);
38
+ return null;
39
+ }
40
+ return flattenSystemRoleDefinitions(Object.values(parsed));
41
+ };
42
+ /**
43
+ * Opens a ScopeService and brings both the scope and the role `declared` marks
44
+ * up to date with the code.
45
+ *
46
+ * Scopes are synced first and not as a courtesy: `pikku_role_scopes` has a
47
+ * foreign key onto `pikku_scopes`, so a role granting a freshly-declared scope
48
+ * cannot be written until that scope's row exists. Both syncs are additive, so
49
+ * doing them inside a read-only command is safe.
50
+ */
51
+ export const openScopeServiceForRoles = async (args, roles, command) => {
52
+ const declaredScopes = await loadDeclaredScopes(args.config.scopesMetaJsonFile, args.logger);
53
+ if (!declaredScopes) {
54
+ throw new Error('scope metadata not found');
55
+ }
56
+ const opened = await openScopeService(args, declaredScopes, command);
57
+ if (!opened) {
58
+ return null;
59
+ }
60
+ try {
61
+ await opened.service.syncSystemRoles(roles);
62
+ }
63
+ catch (e) {
64
+ await opened.destroy();
65
+ throw e;
66
+ }
67
+ return opened;
68
+ };
69
+ /**
70
+ * Renders undeclared system roles and how many people still hold each — the
71
+ * blast radius, shown before anything is deleted.
72
+ */
73
+ export const reportStaleRoles = (stale, logger) => {
74
+ for (const { role, users } of stale) {
75
+ logger.info(` ${role.padEnd(40)} held by ${users} user(s)`);
76
+ }
77
+ };
@@ -8,12 +8,12 @@
8
8
  * unconditionally.
9
9
  */
10
10
  import type { ScenarioBrowserFailure, ScenarioBrowserProvider } from '@pikku/core/workflow';
11
- import type { ScenarioActorConfig } from '@pikku/core/services';
11
+ import type { ResolvedPersona } from '@pikku/core/services';
12
12
  /** The default driver, used when a project names no other. */
13
13
  export declare const DEFAULT_BROWSER_DRIVER = "@pikku/playwright";
14
14
  export interface ScenarioBrowserDriverOptions {
15
15
  secret: string;
16
- actors: Record<string, ScenarioActorConfig>;
16
+ actors: Record<string, ResolvedPersona>;
17
17
  signInPath?: string;
18
18
  failureDir?: string;
19
19
  config: unknown;
@@ -49,7 +49,7 @@ export interface ResolveScenarioBrowserProviderOptions {
49
49
  apiUrl: string;
50
50
  appUrl?: string;
51
51
  secret: string;
52
- actors: Record<string, ScenarioActorConfig>;
52
+ actors: Record<string, ResolvedPersona>;
53
53
  signInPath?: string;
54
54
  /** Where failure screenshots are written. */
55
55
  failureDir: string;
@@ -26,7 +26,7 @@ export const resolveScenarioBrowserProvider = async ({ environment, apiUrl, appU
26
26
  });
27
27
  if (!config.appUrl || config.appUrlSource === 'default') {
28
28
  throw new Error(`Scenario environment '${environment}' has browser steps but no 'appUrl', and '${driver}' resolved none from the environment. ` +
29
- `Add it to scenarios.environments.${environment} in pikku.config.json, pass --app-url for a target that only exists at run time, or run with --no-browser to skip them.`);
29
+ `Add it to environments.${environment} in pikku.config.json, pass --app-url for a target that only exists at run time, or run with --no-browser to skip them.`);
30
30
  }
31
31
  const options = {
32
32
  secret,
@@ -1,4 +1,4 @@
1
- import type { ScenarioStepPhase } from '@pikku/core/workflow';
1
+ import type { ScenarioStepPhase, ScenarioSurface } from '@pikku/core/workflow';
2
2
  import type { WorkflowStepMeta } from '@pikku/core/workflow/types';
3
3
  import type { FunctionsMeta } from '@pikku/core';
4
4
  import type { ScenarioFailureDetail, ScenarioStepRow } from './scenario-formatter.js';
@@ -53,10 +53,52 @@ export interface ScenarioProse {
53
53
  export declare const collectScenarioStepProse: (workflowMeta: {
54
54
  steps?: WorkflowStepMeta[];
55
55
  } | undefined, functionsMeta: FunctionsMeta) => ScenarioProse;
56
- /** The step functions in this scenario that declare `browser: true`. */
56
+ /** The step functions in this scenario that declare a `browser` binding. */
57
57
  export declare const scenarioBrowserSteps: (workflowMeta: {
58
58
  steps?: WorkflowStepMeta[];
59
59
  } | undefined, functionsMeta: FunctionsMeta) => string[];
60
+ /**
61
+ * Steps this scenario cannot run on `surface` at all — they declare neither a
62
+ * binding for it nor a `default` to fall back to.
63
+ *
64
+ * Every phase counts. An action with nothing to run is a broken ladder; an
65
+ * assertion with nothing to run would report a pass having checked nothing,
66
+ * which the engine now refuses outright. Both are better caught here, so the
67
+ * flow is held back with a reason than failed halfway through.
68
+ *
69
+ * A step that *can* run, just not on the run surface, is the different case — a
70
+ * coverage gap, counted by {@link scenarioSurfaceCoverage} rather than a reason
71
+ * to skip.
72
+ */
73
+ export declare const scenarioStepsWithoutBinding: (workflowMeta: {
74
+ steps?: WorkflowStepMeta[];
75
+ } | undefined, functionsMeta: FunctionsMeta, surface: ScenarioSurface) => string[];
76
+ /**
77
+ * How much of this scenario actually ran on the surface the run targeted.
78
+ *
79
+ * Every step counts, not just the assertions. A ladder of
80
+ * `browser/browser/default/browser` is 3/4 — the step that fell back to the
81
+ * server shows up by lowering the ratio, which is a truer signal than a green
82
+ * assertion count with a footnote. It also makes surfaces comparable: the same
83
+ * scenario is 4/4 on a default run and 3/4 on a browser one, over one
84
+ * denominator.
85
+ *
86
+ * Assertions still get named separately. A `given` or `when` driven server-side
87
+ * took a shortcut; a `then` driven server-side is a sentence claiming the actor
88
+ * saw something nobody looked at. Same effect on the ratio, different kind of
89
+ * problem, so `--strict` gates on the latter only.
90
+ */
91
+ export interface ScenarioSurfaceCoverage {
92
+ /** Steps that ran on the run surface. */
93
+ onSurface: number;
94
+ /** Steps that ran at all. */
95
+ total: number;
96
+ /** The `then` steps that ran, but somewhere other than the run surface. */
97
+ unwitnessed: string[];
98
+ }
99
+ export declare const scenarioSurfaceCoverage: (workflowMeta: {
100
+ steps?: WorkflowStepMeta[];
101
+ } | undefined, functionsMeta: FunctionsMeta, runSurface: ScenarioSurface) => ScenarioSurfaceCoverage;
60
102
  /** The run's steps as renderable rows, in the order they were recorded. */
61
103
  export declare const scenarioStepRows: (steps: ScenarioStepOutcome[], prose: ScenarioProse) => ScenarioStepRow[];
62
104
  /**
@@ -76,16 +76,65 @@ const sameProse = (a, b) => a.phase === b.phase &&
76
76
  a.description === b.description &&
77
77
  a.template === b.template &&
78
78
  a.actor === b.actor;
79
- /** The step functions in this scenario that declare `browser: true`. */
79
+ /** The step functions in this scenario that declare a `browser` binding. */
80
80
  export const scenarioBrowserSteps = (workflowMeta, functionsMeta) => {
81
81
  const names = new Set();
82
82
  for (const step of walkScenarioSteps(workflowMeta?.steps)) {
83
- if (functionsMeta[step.stepFunc]?.scenarioStepBrowser === true) {
83
+ if (functionsMeta[step.stepFunc]?.scenarioStepSurfaces?.includes('browser')) {
84
84
  names.add(step.stepFunc);
85
85
  }
86
86
  }
87
87
  return [...names];
88
88
  };
89
+ /**
90
+ * Steps this scenario cannot run on `surface` at all — they declare neither a
91
+ * binding for it nor a `default` to fall back to.
92
+ *
93
+ * Every phase counts. An action with nothing to run is a broken ladder; an
94
+ * assertion with nothing to run would report a pass having checked nothing,
95
+ * which the engine now refuses outright. Both are better caught here, so the
96
+ * flow is held back with a reason than failed halfway through.
97
+ *
98
+ * A step that *can* run, just not on the run surface, is the different case — a
99
+ * coverage gap, counted by {@link scenarioSurfaceCoverage} rather than a reason
100
+ * to skip.
101
+ */
102
+ export const scenarioStepsWithoutBinding = (workflowMeta, functionsMeta, surface) => {
103
+ const names = new Set();
104
+ for (const step of walkScenarioSteps(workflowMeta?.steps)) {
105
+ const surfaces = functionsMeta[step.stepFunc]?.scenarioStepSurfaces ?? [
106
+ 'default',
107
+ ];
108
+ if (!surfaces.includes(surface) && !surfaces.includes('default')) {
109
+ names.add(step.stepFunc);
110
+ }
111
+ }
112
+ return [...names];
113
+ };
114
+ export const scenarioSurfaceCoverage = (workflowMeta, functionsMeta, runSurface) => {
115
+ let onSurface = 0;
116
+ let total = 0;
117
+ const unwitnessed = new Set();
118
+ for (const step of walkScenarioSteps(workflowMeta?.steps)) {
119
+ const surfaces = functionsMeta[step.stepFunc]?.scenarioStepSurfaces ?? [
120
+ 'default',
121
+ ];
122
+ // A step with no binding this run can execute never runs, so it is not a
123
+ // statistic about this run — `scenarioStepsWithoutBinding` holds the flow
124
+ // back for it. Counting it here would score a run on work it refuses to do.
125
+ if (!surfaces.includes(runSurface) && !surfaces.includes('default')) {
126
+ continue;
127
+ }
128
+ total += 1;
129
+ if (runSurface === 'default' || surfaces.includes(runSurface)) {
130
+ onSurface += 1;
131
+ }
132
+ else if (step.phase === 'then') {
133
+ unwitnessed.add(step.stepFunc);
134
+ }
135
+ }
136
+ return { onSurface, total, unwitnessed: [...unwitnessed] };
137
+ };
89
138
  /**
90
139
  * Render one step's gherkin sentence.
91
140
  *
@@ -1,3 +1,4 @@
1
+ import type { ScenarioSurface } from '@pikku/core/workflow';
1
2
  export declare const scenarioList: import("#pikku").PikkuFunctionConfig<{}, void, "rpc" | "session", import("#pikku").PikkuFunctionSessionless<{}, void, "rpc" | "session", import("#pikku").WiredServices> | import("#pikku").PikkuFunction<{}, void, "rpc" | "session", import("#pikku").WiredServices>, undefined, undefined>;
2
3
  export declare const scenarioRun: import("#pikku").PikkuFunctionConfig<{
3
4
  environment: string;
@@ -7,6 +8,8 @@ export declare const scenarioRun: import("#pikku").PikkuFunctionConfig<{
7
8
  excludeTags?: string;
8
9
  coverage?: boolean;
9
10
  browser?: boolean;
11
+ run?: ScenarioSurface;
12
+ strict?: boolean;
10
13
  spawn?: boolean;
11
14
  keepAlive?: boolean;
12
15
  trace?: boolean;
@@ -20,6 +23,8 @@ export declare const scenarioRun: import("#pikku").PikkuFunctionConfig<{
20
23
  excludeTags?: string;
21
24
  coverage?: boolean;
22
25
  browser?: boolean;
26
+ run?: ScenarioSurface;
27
+ strict?: boolean;
23
28
  spawn?: boolean;
24
29
  keepAlive?: boolean;
25
30
  trace?: boolean;
@@ -33,6 +38,8 @@ export declare const scenarioRun: import("#pikku").PikkuFunctionConfig<{
33
38
  excludeTags?: string;
34
39
  coverage?: boolean;
35
40
  browser?: boolean;
41
+ run?: ScenarioSurface;
42
+ strict?: boolean;
36
43
  spawn?: boolean;
37
44
  keepAlive?: boolean;
38
45
  trace?: boolean;