@reticlehq/server 2.9.0 → 2.11.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.
Files changed (125) hide show
  1. package/SKILL.md +64 -26
  2. package/dist/capsule/capsule-store.d.ts +2 -2
  3. package/dist/cli/cli-parse.d.ts +1 -1
  4. package/dist/cli/cli-parse.js +1 -1
  5. package/dist/cli/cli-verify.js +1 -1
  6. package/dist/crawl/crawl.d.ts +7 -2
  7. package/dist/crawl/crawl.js +52 -9
  8. package/dist/dev/stale-issue-guard.d.ts +43 -0
  9. package/dist/dev/stale-issue-guard.js +75 -0
  10. package/dist/domain/domain-tools.js +3 -2
  11. package/dist/ee/audit-log.js +2 -2
  12. package/dist/events/body-failures.d.ts +2 -2
  13. package/dist/events/contradiction-folds.d.ts +45 -0
  14. package/dist/events/contradiction-folds.js +42 -0
  15. package/dist/events/contradictions.d.ts +19 -1
  16. package/dist/events/contradictions.js +17 -2
  17. package/dist/events/echo-mismatch.d.ts +2 -2
  18. package/dist/events/predicate-asks.d.ts +17 -0
  19. package/dist/events/predicate-asks.js +26 -0
  20. package/dist/events/predicate-eval.js +211 -24
  21. package/dist/events/predicate-parse.js +9 -3
  22. package/dist/events/predicate-schema.d.ts +18 -0
  23. package/dist/events/predicate-schema.js +55 -0
  24. package/dist/events/stale-response.d.ts +2 -3
  25. package/dist/events/stale-response.js +49 -2
  26. package/dist/events/unit-mismatch.d.ts +2 -2
  27. package/dist/flows/flow-tools.js +34 -5
  28. package/dist/flows/predicate-to-expect.js +8 -0
  29. package/dist/flows/verify-change-tools.js +6 -2
  30. package/dist/honesty/blind-spots.d.ts +9 -0
  31. package/dist/honesty/blind-spots.js +17 -4
  32. package/dist/honesty/gap-ledger.d.ts +52 -0
  33. package/dist/honesty/gap-ledger.js +63 -0
  34. package/dist/honesty/instrumentation-gaps.d.ts +55 -0
  35. package/dist/honesty/instrumentation-gaps.js +21 -0
  36. package/dist/honesty/observability.d.ts +37 -0
  37. package/dist/honesty/observability.js +40 -0
  38. package/dist/honesty/verified.js +16 -0
  39. package/dist/index.d.ts +18 -1
  40. package/dist/index.js +57 -0
  41. package/dist/init/mcp.d.ts +1 -2
  42. package/dist/init/mcp.js +4 -1
  43. package/dist/init/node-io.js +3 -0
  44. package/dist/init/run.d.ts +18 -0
  45. package/dist/init/run.js +34 -3
  46. package/dist/init/slash-command.d.ts +1 -1
  47. package/dist/init/slash-command.js +33 -10
  48. package/dist/journal/envelope.d.ts +24 -24
  49. package/dist/license/license.d.ts +76 -5
  50. package/dist/license/license.js +93 -29
  51. package/dist/mcp/mcp-proxy.d.ts +2 -27
  52. package/dist/mcp/mcp-proxy.js +4 -88
  53. package/dist/mcp/mcp.d.ts +18 -2
  54. package/dist/mcp/mcp.js +31 -10
  55. package/dist/mcp/proxy-daemon-probe.d.ts +8 -0
  56. package/dist/mcp/proxy-daemon-probe.js +48 -0
  57. package/dist/mcp/proxy-handshake.js +1 -1
  58. package/dist/mcp/server-instructions.js +2 -2
  59. package/dist/mcp/sse-frame-parser.d.ts +19 -0
  60. package/dist/mcp/sse-frame-parser.js +41 -0
  61. package/dist/project/artifact-root.d.ts +83 -0
  62. package/dist/project/artifact-root.js +112 -0
  63. package/dist/project/memory-fs.d.ts +9 -0
  64. package/dist/project/memory-fs.js +88 -0
  65. package/dist/project/project-store.d.ts +17 -0
  66. package/dist/project/project-store.js +33 -0
  67. package/dist/project/remember-project.d.ts +24 -0
  68. package/dist/project/remember-project.js +40 -0
  69. package/dist/project/session-root.d.ts +15 -0
  70. package/dist/project/session-root.js +31 -0
  71. package/dist/runs/run-tools.js +2 -1
  72. package/dist/session/lease-availability.d.ts +40 -0
  73. package/dist/session/lease-availability.js +43 -0
  74. package/dist/session/no-session-diagnosis.d.ts +17 -0
  75. package/dist/session/no-session-diagnosis.js +41 -8
  76. package/dist/session/no-session-watch.js +7 -0
  77. package/dist/session/session-readiness.js +1 -1
  78. package/dist/session/session.d.ts +9 -0
  79. package/dist/session/session.js +9 -0
  80. package/dist/session/stall-clock.d.ts +18 -0
  81. package/dist/session/stall-clock.js +48 -0
  82. package/dist/telemetry/daemon-telemetry.js +16 -22
  83. package/dist/telemetry/license-activation.d.ts +48 -0
  84. package/dist/telemetry/license-activation.js +57 -0
  85. package/dist/telemetry/session-metrics.d.ts +1 -1
  86. package/dist/telemetry/session-metrics.js +1 -1
  87. package/dist/telemetry/telemetry.d.ts +12 -3
  88. package/dist/telemetry/telemetry.js +34 -5
  89. package/dist/tools/act-tools.js +34 -4
  90. package/dist/tools/assert-verdict.d.ts +2 -0
  91. package/dist/tools/assert-verdict.js +24 -1
  92. package/dist/tools/contract-tools.js +5 -3
  93. package/dist/tools/coverage-tools.js +33 -0
  94. package/dist/tools/dynamic-tools.js +1 -1
  95. package/dist/tools/error-recovery.js +1 -1
  96. package/dist/tools/feedback-tools.js +31 -6
  97. package/dist/tools/invoke-tool.d.ts +1 -1
  98. package/dist/tools/invoke-tool.js +10 -5
  99. package/dist/tools/observe-tools.js +8 -2
  100. package/dist/tools/tool-kit.d.ts +46 -4
  101. package/dist/tools/tool-names.d.ts +2 -0
  102. package/dist/tools/tool-names.js +2 -0
  103. package/dist/tools/tool-surface.js +2 -3
  104. package/dist/tools/tools.js +16 -1
  105. package/docs/README.md +1 -0
  106. package/docs/agent-cheatsheet.md +2 -2
  107. package/docs/capabilities.mdx +4 -4
  108. package/docs/cli/hunt.mdx +2 -2
  109. package/docs/deploy-checks.md +2 -2
  110. package/docs/docs.json +2 -2
  111. package/docs/enterprise.md +9 -3
  112. package/docs/flows.md +4 -4
  113. package/docs/gates.md +1 -1
  114. package/docs/license-activation.mdx +145 -0
  115. package/docs/recipes.mdx +2 -2
  116. package/docs/telemetry-contract.md +18 -12
  117. package/docs/telemetry-events.mdx +1 -34
  118. package/docs/telemetry.md +0 -1
  119. package/docs/tools-overview.mdx +1 -1
  120. package/docs/tools-session-and-feedback.mdx +5 -0
  121. package/docs/tools-tools-and-run.mdx +15 -15
  122. package/docs/usage.md +3 -3
  123. package/package.json +2 -2
  124. package/dist/telemetry/instrumentation-stall.d.ts +0 -50
  125. package/dist/telemetry/instrumentation-stall.js +0 -90
package/dist/index.js CHANGED
@@ -1,4 +1,9 @@
1
1
  import { join } from 'node:path';
2
+ import { existsSync, readFileSync } from 'node:fs';
3
+ import { homedir } from 'node:os';
4
+ import { PROJECT_REGISTRY_FILE, emptyProjectRegistry, parseProjectRegistry } from '@reticlehq/core';
5
+ import { discoverProjectConfigs } from './cli/config-discovery.js';
6
+ import { projectCandidatesFrom, resolveArtifactRoot, } from './project/artifact-root.js';
2
7
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
8
  import { AGENT_STOPPED_NOTICE, RETICLE_DEFAULT_PORT, ReticleCommand, ReticleDir, ReticleEnv, LOOPBACK_HOST, ReplayStatus, EventType, } from '@reticlehq/core';
4
9
  import { originOf } from './session/session-manager.js';
@@ -72,6 +77,22 @@ export { ProjectStore } from './project/project-store.js';
72
77
  export { VisualStore } from './visual/visual-store.js';
73
78
  export { diffPng } from './visual/visual-diff.js';
74
79
  export { crawl } from './crawl/crawl.js';
80
+ /**
81
+ * The contradiction pass, and the seam a consumer adds its own rules through.
82
+ *
83
+ * Exported because a service embedding this engine grades a recording the same way the tools do, and
84
+ * without these it would have to re-implement the fold — a second implementation of the one thing
85
+ * this product is judged on, and the one nobody dogfoods is the one that rots.
86
+ */
87
+ export { findContradictions } from './events/contradictions.js';
88
+ /**
89
+ * The MCP server factory, so a consumer can serve the tool surface it composed.
90
+ *
91
+ * Without this the composition seam is unreachable from outside the package: a consumer can build the
92
+ * list and has nothing to hand it to.
93
+ */
94
+ export { createMcpServer } from './mcp/mcp.js';
95
+ export { registerContradictionFold, registeredContradictionFolds, } from './events/contradiction-folds.js';
75
96
  export { MCP_SSE_PATH, MCP_MESSAGE_PATH } from '@reticlehq/core';
76
97
  export { BrowserPool, DEFAULT_LEASE_TTL_MS } from './pool/browser-pool.js';
77
98
  export { playwrightLauncher, resolveMaxContexts } from './pool/playwright-launcher.js';
@@ -235,6 +256,41 @@ async function resolveRealInput(options, onNavigateError, onNetworkDetail) {
235
256
  return {};
236
257
  }
237
258
  /** Start the Reticle bridge (browser WS endpoint) and, by default, the MCP stdio server. */
259
+ /**
260
+ * Resolve a session's artifact root, from everything this machine knows about where projects live.
261
+ *
262
+ * Built once per daemon and closed over by every tool call. The two sources are read lazily and
263
+ * cheaply on each call rather than snapshotted at startup: `init` can run in another terminal while
264
+ * this daemon is up, and a resolution that used a startup snapshot would keep answering with a map
265
+ * from before the project the agent is now driving existed.
266
+ */
267
+ function artifactRootResolver(daemonRoot) {
268
+ return (projectId) => {
269
+ let registry = emptyProjectRegistry();
270
+ try {
271
+ const path = join(homedir(), ReticleDir.ROOT, PROJECT_REGISTRY_FILE);
272
+ registry = existsSync(path)
273
+ ? parseProjectRegistry(JSON.parse(readFileSync(path, 'utf8')))
274
+ : registry;
275
+ }
276
+ catch {
277
+ // A cache that cannot be read is an empty cache, never an error: the daemon still resolves
278
+ // through discovery, and falls back to its own root exactly as it did before this existed.
279
+ }
280
+ let discovery = { found: [], searched: [] };
281
+ try {
282
+ discovery = discoverProjectConfigs(process.cwd());
283
+ }
284
+ catch {
285
+ // Same reasoning: a diagnostic search that throws must not take a tool call with it.
286
+ }
287
+ return resolveArtifactRoot({
288
+ projectId,
289
+ candidates: projectCandidatesFrom(discovery, registry),
290
+ daemonRoot,
291
+ });
292
+ };
293
+ }
238
294
  export async function start(options = {}) {
239
295
  const port = options.port ?? RETICLE_DEFAULT_PORT;
240
296
  // Open the user's impact record before anything can connect. Not inside the MCP branch: a daemon
@@ -286,6 +342,7 @@ export async function start(options = {}) {
286
342
  project,
287
343
  fs,
288
344
  reticleRoot,
345
+ artifactRootFor: artifactRootResolver(reticleRoot),
289
346
  now,
290
347
  bridgePort: port,
291
348
  browserProbe: probeChromium,
@@ -11,7 +11,7 @@
11
11
  * project's `.reticle.json` in its CWD (see cli-port.ts). The port belongs to the project, not the
12
12
  * global agent config.
13
13
  */
14
- export declare const MCP_SERVER_NAME = "reticle";
14
+ export { MCP_SERVER_NAME } from '@reticlehq/core';
15
15
  /**
16
16
  * The registered command is STILL bare `npx` on every platform, deliberately.
17
17
  *
@@ -76,4 +76,3 @@ export declare function claudeAvailableProbe(): {
76
76
  };
77
77
  /** Printed when the `claude` CLI isn't available — register Reticle globally once, by hand. */
78
78
  export declare function mcpManual(): string;
79
- export {};
package/dist/init/mcp.js CHANGED
@@ -12,7 +12,10 @@
12
12
  * global agent config.
13
13
  */
14
14
  import { RETICLE_NPM_PACKAGE } from '../version/server-version.js';
15
- export const MCP_SERVER_NAME = 'reticle';
15
+ // The name is a wire identity and lives in the contract; re-exported here so the registration writers
16
+ // below (and their tests) keep reading it from the module that USES it.
17
+ export { MCP_SERVER_NAME } from '@reticlehq/core';
18
+ import { MCP_SERVER_NAME } from '@reticlehq/core';
16
19
  /**
17
20
  * The registered command is STILL bare `npx` on every platform, deliberately.
18
21
  *
@@ -52,6 +52,9 @@ export function buildNodeIo(cwd) {
52
52
  homeDir() {
53
53
  return homedir();
54
54
  },
55
+ cwd() {
56
+ return cwd;
57
+ },
55
58
  rootFiles() {
56
59
  return readdirSync(cwd).filter((name) => {
57
60
  try {
@@ -3,6 +3,8 @@
3
3
  * plan (pure), optionally write the apply-steps, and print a human-readable report. All filesystem
4
4
  * access goes through `InitIo` so the orchestration is unit-testable with an in-memory IO.
5
5
  */
6
+ import { Framework } from './detect.js';
7
+ import { StepStatus } from './plan.js';
6
8
  import type { InitOutcome } from '@reticlehq/core';
7
9
  /**
8
10
  * Resolve the lockfiles set used to pick the package manager. A lockfile in the project root wins;
@@ -56,6 +58,8 @@ export interface InitIo {
56
58
  exists(relPath: string): boolean;
57
59
  /** The user's home directory (for global agent config like ~/.cursor/mcp.json). */
58
60
  homeDir(): string;
61
+ /** Absolute path of the directory `init` is running in — the project's own root. */
62
+ cwd(): string;
59
63
  /** Basenames present in the project root. */
60
64
  rootFiles(): readonly string[];
61
65
  /** Subdirectory names inside a project-relative directory; empty when it isn't one. */
@@ -99,4 +103,18 @@ export declare function findWorkspaceApps(io: Pick<InitIo, 'exists' | 'readFile'
99
103
  * the workspace redirect is kept from asking twice.
100
104
  */
101
105
  export declare const FEEDBACK_HINT: string;
106
+ /**
107
+ * The last two lines a user reads, and the order matters.
108
+ *
109
+ * This used to say only "Restart <dev server>, then ask your agent: List Reticle sessions" — and
110
+ * asking the agent was the one thing that could not work yet. `init` registers the MCP server, but
111
+ * an agent client reads its tool list when it STARTS and never re-reads it, so the session that
112
+ * just ran `init` has no `reticle_*` tools however clean the install was. The user follows the
113
+ * instruction, the agent answers "unknown tool", and the obvious conclusion is that the install
114
+ * failed.
115
+ *
116
+ * So the reload is named FIRST, before the sentence that depends on it. Omitted entirely when this
117
+ * run did not register MCP (`--no-mcp`), where it would be advice about something we did not do.
118
+ */
119
+ export declare function restartHint(framework: Framework, mcpStatus: StepStatus | undefined): string;
102
120
  export declare function runInit(options: InitOptions, io: InitIo): InitResult;
package/dist/init/run.js CHANGED
@@ -8,6 +8,7 @@ import { noPackageJsonMessage } from './non-js-project.js';
8
8
  import { spanSync } from '../trace.js';
9
9
  import { readFileSync } from 'node:fs';
10
10
  import { homedir } from 'node:os';
11
+ import { projectIdOf, rememberProjectOnDisk } from '../project/remember-project.js';
11
12
  import { detect, Framework, namesAPackageManager, UiLibrary } from './detect.js';
12
13
  import { wasMcpRegistered } from './mcp-registered.js';
13
14
  import { pickAstroHost } from './astro-host.js';
@@ -504,7 +505,7 @@ function devServerRestart(framework) {
504
505
  * So the reload is named FIRST, before the sentence that depends on it. Omitted entirely when this
505
506
  * run did not register MCP (`--no-mcp`), where it would be advice about something we did not do.
506
507
  */
507
- function restartHint(framework, mcpRegistered) {
508
+ export function restartHint(framework, mcpStatus) {
508
509
  const dev = `${devServerRestart(framework)}.`;
509
510
  // NAME THE COMMAND THAT PROVES IT, not one that merely asks.
510
511
  //
@@ -518,8 +519,27 @@ function restartHint(framework, mcpRegistered) {
518
519
  // running, an app running that never dialled us, a tab that closed — with the fix for each.
519
520
  const prove = 'Then run `npx @reticlehq/server status` — it confirms the app connected, or says exactly why ' +
520
521
  'it has not.';
521
- if (!mcpRegistered)
522
+ // Nothing registered this run (`--no-mcp`, or a client that needs a manual edit): a restart picks
523
+ // up nothing, so advice about one would be advice about something we did not do.
524
+ if (!wasMcpRegistered(mcpStatus))
522
525
  return `${dev} ${prove}`;
526
+ // ALREADY: the tools are reachable RIGHT NOW, and this is the branch that used to lie.
527
+ //
528
+ // `wasMcpRegistered` is true for both APPLY and ALREADY, which is correct for the funnel field it
529
+ // was written for and wrong here, because the two cases have opposite answers to "must anybody
530
+ // restart". Every second project on a machine, and every Claude Code plugin install, took the
531
+ // APPLY text: "restart your agent — the tools only appear after that". An agent reads this AFTER
532
+ // whatever its skill file said, and it describes the output in front of it, so it wins. The agent
533
+ // stops with the project wired and nothing driven, which is the exact failure the install path
534
+ // exists to prevent, caused by the install path itself.
535
+ if (mcpStatus === StepStatus.ALREADY) {
536
+ return (`${dev}\n` +
537
+ 'The Reticle MCP server was already registered on this machine, so there is no restart to do ' +
538
+ 'and the tools are available now.\n' +
539
+ `${prove}\n` +
540
+ 'Once it shows a session, drive a flow — that is the install finished. Carry straight on; ' +
541
+ 'nothing here is waiting on a human.');
542
+ }
523
543
  return (`${dev}\n` +
524
544
  'Then restart your agent so it picks up the new MCP server — restart Claude Code, reload the ' +
525
545
  'window in Cursor, or hit Start in `.vscode/mcp.json` in VS Code.\n' +
@@ -592,7 +612,7 @@ function report(plan, dryRun, failed, skipped, degraded, io, projectDir, agentRo
592
612
  '"no browser session connected" until then. Everything else is already in place.');
593
613
  io.print('');
594
614
  }
595
- io.print(restartHint(plan.framework, wasMcpRegistered(resolvedStatus(plan, MCP_TARGET, failed, skipped))));
615
+ io.print(restartHint(plan.framework, resolvedStatus(plan, MCP_TARGET, failed, skipped)));
596
616
  return { ok: !connectPending, applied, manual };
597
617
  }
598
618
  /**
@@ -684,6 +704,17 @@ function applyEffects(plan, io) {
684
704
  installFailed = true;
685
705
  }
686
706
  }
707
+ // Where this project lives, remembered for a daemon that will be started somewhere else.
708
+ //
709
+ // Deliberately AFTER the loop and unconditional on which steps ran: most init runs report "already
710
+ // wired", and a re-run in a re-cloned or moved checkout is exactly when the remembered path has
711
+ // gone stale — so the run that would otherwise be a no-op is the one that repairs the entry. The
712
+ // return value is ignored on purpose; this is a cache, and a read-only home directory must not
713
+ // turn a wired project into a failed init.
714
+ const projectId = projectIdOf(io.readFile(RETICLE_CONFIG_FILE));
715
+ if (projectId !== undefined) {
716
+ rememberProjectOnDisk(io, projectId, io.cwd(), Date.now());
717
+ }
687
718
  return { failed, skipped, degraded };
688
719
  }
689
720
  /**
@@ -24,4 +24,4 @@ export declare const CURSOR_COMMAND_PATH = ".cursor/commands/reticle.md";
24
24
  * are told apart.
25
25
  */
26
26
  export declare const SLASH_COMMAND_SIGNATURE = "description: Verify this app in the browser with Reticle";
27
- export declare const SLASH_COMMAND_BODY = "---\ndescription: Verify this app in the browser with Reticle \u2014 drive one real flow and report what happened.\n---\n\nVerify this running app with Reticle. Drive it; do not read the code and guess.\n\n## Step 0 \u2014 is anything connected?\n\nCall `reticle_sessions` first.\n\n**A session is listed?** Go to \"Pick ONE flow\".\n\n**No session?** Setup is not finished, and stopping to ask the user to run something is not an\nanswer. Work these in order, and say which one you are on:\n\n0. **Is anything serving the app at all?** If a dev server is already listening, use it and go to\n step 1 \u2014 the cause is then almost always that the SDK never loaded in the page, not a missing\n server, so do not tell the user to start one they are already running. If nothing is listening:\n\n**If no dev server is listening, start one yourself.** Read the project's own dev script out of `package.json` (`dev`, `start`, whatever this project calls it), run it in the BACKGROUND, tell the user in one line that it is running and how to stop it, then carry on. Stopping to ask is how a verification turn ends with nothing verified.\n\nFive guards, none optional:\n\n1. **Never start a second one.** If something is already listening on the app's port, use it.\n2. **Never guess the command.** It comes from `package.json` scripts. If there is no recognisable dev script, say so and stop rather than inventing one.\n3. **Never kill anything.** Not a dev server, not a daemon, not a port holder \u2014 including one you started.\n4. **Background it, and say so.** The user must know a server is running and how to stop it. A dev server the human does not know about is the same failure one step later.\n5. **The permission prompt belongs to your host.** Never try to bypass, suppress or auto-approve it, and take a refusal as the answer.\n\nThen, once something is serving the app:\n\n1. **Is the SDK actually in the app?** Read the app's entry file (`src/main.tsx`, `app/layout.tsx`,\n `src/app.vue`, whatever this project uses). You are looking for an `import` from\n `@reticlehq/react` or `@reticlehq/browser` and a `connect()` call, or the Reticle plugin in\n `vite.config.*`, or `withReticle` in `next.config.*`. **If it is not there, that is the bug.**\n Wire it in \u2014 see the framework snippets at https://docs.reticle.sh/instrumentation \u2014 then restart\n the dev server and re-check.\n2. **Is the connect guarded on `hostname === 'localhost'`?** If so, remove that guard. It is false on\n any non-localhost dev host, and `window` does not exist during SSR, so the guard silently prevents\n the connect and logs nothing. Use the framework's dev flag instead (`import.meta.env.DEV`,\n `process.env.NODE_ENV !== 'production'`, `import.meta.dev`).\n3. **Is the dev server serving that entry?** Ask the user to open the app in a browser and check its\n console for a line starting `[Reticle]`. That line, or its absence, tells you which side is at\n fault. Reticle itself never starts a dev server \u2014 starting it is your job, under the guards above.\n4. **Do both sides agree on the port?** Run `npx @reticlehq/server doctor` and compare the daemon\n port against the port the page dials.\n\nIf you are still stuck after all four, file it: `reticle_feedback` with kind `gap`, saying what you\nchecked and what you saw. Then tell the user exactly which step is blocked. Do not report the install\nas finished.\n\n## Pick ONE flow\n\nNot the whole app. Pick the single most important flow you can complete in a handful of steps \u2014 the\none a user would do first (sign in, search, add to cart, submit the form). If the user named a flow or\njust changed some code, use that one instead.\n\nState which flow you picked in one line before you start.\n\n## Drive it where the human can see\n\n1. `reticle_snapshot` \u2014 read the accessibility tree and locate the elements the flow needs. Elements\n are addressable by role and name; you do **not** need `data-testid` to drive them.\n2. Walk the flow with `reticle_act_and_wait`, one step at a time. Narrate each step first \u2014 the human\n is watching the page, and the HUD shows what you say.\n3. After each step, check the effect with `reticle_assert` \u2014 not just that the click dispatched, but\n that the thing it was supposed to do actually happened.\n4. Finish with `reticle_console` and `reticle_network` to catch errors the DOM does not show.\n\n**Only `reticle_act_and_wait` and `reticle_assert` produce a verdict.** A run that ends on a\nsnapshot has proved nothing, however many tools it used.\n\n## Report\n\n- What you drove, step by step, and what each step actually produced.\n- Anything broken, with the `file:line` Reticle gave you.\n- If a step could not be verified, say so plainly. \"Unknown\" is a real answer; a green verdict you\n cannot back is not.\n\nDo not weaken or skip an assertion to make the run pass \u2014 that is a finding, not a fix.\n\n## Two things afterwards\n\n- **Anything about Reticle itself that was wrong, missing, confusing, or expensive?** Send it with\n `reticle_feedback` now, before your context is gone. That report is the only signal that decides\n what gets fixed. Bugs in the app under test are Reticle working; those go in your answer to the user.\n- **If the verification actually succeeded**, add one line for the human: Reticle is open source at\n https://github.com/reticlehq/reticle, and a star helps other people find it. One line, once. Skip it\n entirely if the run did not produce a verdict.\n";
27
+ export declare const SLASH_COMMAND_BODY = "---\ndescription: Verify this app in the browser with Reticle \u2014 drive one real flow and report what happened.\n---\n\nVerify this running app with Reticle. Drive it; do not read the code and guess.\n\n## Step 0 \u2014 is anything connected?\n\nCall `reticle_sessions` first.\n\n**A session is listed?** Go to \"Pick ONE flow\".\n\n**No session?** Setup is not finished, and stopping to ask the user to run something is not an\nanswer. Every step below is yours to do without them. Work these in order, and say which one you are\non, one line each:\n\n0. **Is Reticle even wired into this project?** `cat .reticle.json`. If it is not there, this\n project was never set up: run `npx @reticlehq/server@latest init`,\n fix every `\u26A0` and `\u2139` it prints, and carry on. Do not ask first, and do not ask which\n framework or port \u2014 both come out of the repo you are sitting in.\n\n1. **Is anything serving the app at all?** If a dev server is already listening, use it and go to\n step 2 \u2014 the cause is then almost always that the SDK never loaded in the page, not a missing\n server, so do not tell the user to start one they are already running. If nothing is listening:\n\n**If no dev server is listening, start one yourself.** Read the project's own dev script out of `package.json` (`dev`, `start`, whatever this project calls it), run it in the BACKGROUND, tell the user in one line that it is running and how to stop it, then carry on. Stopping to ask is how a verification turn ends with nothing verified.\n\nFive guards, none optional:\n\n1. **Never start a second one.** If something is already listening on the app's port, use it.\n2. **Never guess the command.** It comes from `package.json` scripts. If there is no recognisable dev script, say so and stop rather than inventing one.\n3. **Never kill anything.** Not a dev server, not a daemon, not a port holder \u2014 including one you started.\n4. **Background it, and say so.** The user must know a server is running and how to stop it. A dev server the human does not know about is the same failure one step later.\n5. **The permission prompt belongs to your host.** Never try to bypass, suppress or auto-approve it, and take a refusal as the answer.\n\n2. **Is a page open on it?** Do not ask the user to open a browser \u2014 open it:\n\n ```bash\n npx @reticlehq/server open <the url the dev server is serving>\n ```\n\n That reuses an already-connected tab or opens a new one, and waits for the page to register. On a\n machine with no browser to open, take a tab Reticle owns instead:\n `reticle_run({ tool: \"reticle_lease\", args: { action: \"acquire\", url: \"<the same url>\" } })`.\n\nThen, once something is serving the app and a page is on it:\n\n3. **Is the SDK actually in the app?** Read the app's entry file (`src/main.tsx`, `app/layout.tsx`,\n `src/app.vue`, whatever this project uses). You are looking for an `import` from\n `@reticlehq/react` or `@reticlehq/browser` and a `connect()` call, or the Reticle plugin in\n `vite.config.*`, or `withReticle` in `next.config.*`. **If it is not there, that is the bug.**\n Wire it in \u2014 see the framework snippets at https://docs.reticle.sh/instrumentation \u2014 then restart\n the dev server and re-check.\n4. **Is the connect guarded on `hostname === 'localhost'`?** If so, remove that guard. It is false on\n any non-localhost dev host, and `window` does not exist during SSR, so the guard silently prevents\n the connect and logs nothing. Use the framework's dev flag instead (`import.meta.env.DEV`,\n `process.env.NODE_ENV !== 'production'`, `import.meta.dev`).\n5. **Is the dev server serving that entry?** Look in `reticle_console`, or in the page you opened in\n step 2, for a line starting `[Reticle]`. That line, or its absence, tells you which side is at\n fault. Reticle itself never starts a dev server \u2014 starting it is your job, under the guards above.\n6. **Do both sides agree on the port?** Run `npx @reticlehq/server doctor` and compare the daemon\n port against the port the page dials.\n\nIf you are still stuck after all of those, file it: `reticle_feedback` with kind `gap`, saying what you\nchecked and what you saw. Then tell the user exactly which step is blocked. Do not report the install\nas finished.\n\n## Pick ONE flow\n\nNot the whole app. Pick the single most important flow you can complete in a handful of steps \u2014 the\none a user would do first (sign in, search, add to cart, submit the form). If the user named a flow or\njust changed some code, use that one instead.\n\nState which flow you picked in one line before you start.\n\n## Drive it where the human can see\n\n1. `reticle_snapshot` \u2014 read the accessibility tree and locate the elements the flow needs. Elements\n are addressable by role and name; you do **not** need `data-testid` to drive them.\n2. Walk the flow with `reticle_act_and_wait`, one step at a time. Narrate each step first \u2014 the human\n is watching the page, and the HUD shows what you say.\n3. After each step, check the effect with `reticle_assert` \u2014 not just that the click dispatched, but\n that the thing it was supposed to do actually happened.\n4. Finish with `reticle_console` and `reticle_network` to catch errors the DOM does not show.\n\n**Only `reticle_act_and_wait` and `reticle_assert` produce a verdict.** A run that ends on a\nsnapshot has proved nothing, however many tools it used.\n\n## Report\n\n- What you drove, step by step, and what each step actually produced.\n- Anything broken, with the `file:line` Reticle gave you.\n- If a step could not be verified, say so plainly. \"Unknown\" is a real answer; a green verdict you\n cannot back is not.\n\nDo not weaken or skip an assertion to make the run pass \u2014 that is a finding, not a fix.\n\n## If the user handed you a license key\n\nAppend `RETICLE_LICENSE_KEY=<key>` to the project's `.env` (create it if absent), make sure `.env`\nis in `.gitignore`, and confirm with `npx @reticlehq/server license`. The CLI folds a project-local\n`.env` in on every invocation, so nothing else changes. Never echo the key back, and never put it in\na commit, a comment, or a feedback report.\n\n## Two things afterwards\n\n- **Anything about Reticle itself that was wrong, missing, confusing, or expensive?** Send it with\n `reticle_feedback` now, before your context is gone. That report is the only signal that decides\n what gets fixed. Bugs in the app under test are Reticle working; those go in your answer to the user.\n- **If the verification actually succeeded**, add one line for the human: Reticle is open source at\n https://github.com/reticlehq/reticle, and a star helps other people find it. One line, once. Skip it\n entirely if the run did not produce a verdict.\n";
@@ -38,33 +38,49 @@ Call \`reticle_sessions\` first.
38
38
  **A session is listed?** Go to "Pick ONE flow".
39
39
 
40
40
  **No session?** Setup is not finished, and stopping to ask the user to run something is not an
41
- answer. Work these in order, and say which one you are on:
41
+ answer. Every step below is yours to do without them. Work these in order, and say which one you are
42
+ on, one line each:
42
43
 
43
- 0. **Is anything serving the app at all?** If a dev server is already listening, use it and go to
44
- step 1 — the cause is then almost always that the SDK never loaded in the page, not a missing
44
+ 0. **Is Reticle even wired into this project?** \`cat .reticle.json\`. If it is not there, this
45
+ project was never set up: run \`npx @reticlehq/server@latest init\`,
46
+ fix every \`⚠\` and \`ℹ\` it prints, and carry on. Do not ask first, and do not ask which
47
+ framework or port — both come out of the repo you are sitting in.
48
+
49
+ 1. **Is anything serving the app at all?** If a dev server is already listening, use it and go to
50
+ step 2 — the cause is then almost always that the SDK never loaded in the page, not a missing
45
51
  server, so do not tell the user to start one they are already running. If nothing is listening:
46
52
 
47
53
  ${DEV_SERVER_POLICY}
48
54
 
49
- Then, once something is serving the app:
55
+ 2. **Is a page open on it?** Do not ask the user to open a browser — open it:
56
+
57
+ \`\`\`bash
58
+ npx @reticlehq/server open <the url the dev server is serving>
59
+ \`\`\`
60
+
61
+ That reuses an already-connected tab or opens a new one, and waits for the page to register. On a
62
+ machine with no browser to open, take a tab Reticle owns instead:
63
+ \`reticle_run({ tool: "reticle_lease", args: { action: "acquire", url: "<the same url>" } })\`.
50
64
 
51
- 1. **Is the SDK actually in the app?** Read the app's entry file (\`src/main.tsx\`, \`app/layout.tsx\`,
65
+ Then, once something is serving the app and a page is on it:
66
+
67
+ 3. **Is the SDK actually in the app?** Read the app's entry file (\`src/main.tsx\`, \`app/layout.tsx\`,
52
68
  \`src/app.vue\`, whatever this project uses). You are looking for an \`import\` from
53
69
  \`@reticlehq/react\` or \`@reticlehq/browser\` and a \`connect()\` call, or the Reticle plugin in
54
70
  \`vite.config.*\`, or \`withReticle\` in \`next.config.*\`. **If it is not there, that is the bug.**
55
71
  Wire it in — see the framework snippets at https://docs.reticle.sh/instrumentation — then restart
56
72
  the dev server and re-check.
57
- 2. **Is the connect guarded on \`hostname === 'localhost'\`?** If so, remove that guard. It is false on
73
+ 4. **Is the connect guarded on \`hostname === 'localhost'\`?** If so, remove that guard. It is false on
58
74
  any non-localhost dev host, and \`window\` does not exist during SSR, so the guard silently prevents
59
75
  the connect and logs nothing. Use the framework's dev flag instead (\`import.meta.env.DEV\`,
60
76
  \`process.env.NODE_ENV !== 'production'\`, \`import.meta.dev\`).
61
- 3. **Is the dev server serving that entry?** Ask the user to open the app in a browser and check its
62
- console for a line starting \`[Reticle]\`. That line, or its absence, tells you which side is at
77
+ 5. **Is the dev server serving that entry?** Look in \`reticle_console\`, or in the page you opened in
78
+ step 2, for a line starting \`[Reticle]\`. That line, or its absence, tells you which side is at
63
79
  fault. Reticle itself never starts a dev server — starting it is your job, under the guards above.
64
- 4. **Do both sides agree on the port?** Run \`npx @reticlehq/server doctor\` and compare the daemon
80
+ 6. **Do both sides agree on the port?** Run \`npx @reticlehq/server doctor\` and compare the daemon
65
81
  port against the port the page dials.
66
82
 
67
- If you are still stuck after all four, file it: \`reticle_feedback\` with kind \`gap\`, saying what you
83
+ If you are still stuck after all of those, file it: \`reticle_feedback\` with kind \`gap\`, saying what you
68
84
  checked and what you saw. Then tell the user exactly which step is blocked. Do not report the install
69
85
  as finished.
70
86
 
@@ -98,6 +114,13 @@ snapshot has proved nothing, however many tools it used.
98
114
 
99
115
  Do not weaken or skip an assertion to make the run pass — that is a finding, not a fix.
100
116
 
117
+ ## If the user handed you a license key
118
+
119
+ Append \`RETICLE_LICENSE_KEY=<key>\` to the project's \`.env\` (create it if absent), make sure \`.env\`
120
+ is in \`.gitignore\`, and confirm with \`npx @reticlehq/server license\`. The CLI folds a project-local
121
+ \`.env\` in on every invocation, so nothing else changes. Never echo the key back, and never put it in
122
+ a commit, a comment, or a feedback report.
123
+
101
124
  ## Two things afterwards
102
125
 
103
126
  - **Anything about Reticle itself that was wrong, missing, confusing, or expensive?** Send it with
@@ -43,14 +43,14 @@ export declare const RouteEnvelopeSchema: z.ZodObject<{
43
43
  max: z.ZodNumber;
44
44
  }, "strip", z.ZodTypeAny, {
45
45
  count: number;
46
+ max: number;
46
47
  mean: number;
47
48
  m2: number;
48
- max: number;
49
49
  }, {
50
50
  count: number;
51
+ max: number;
51
52
  mean: number;
52
53
  m2: number;
53
- max: number;
54
54
  }>;
55
55
  net: z.ZodObject<{
56
56
  count: z.ZodNumber;
@@ -59,14 +59,14 @@ export declare const RouteEnvelopeSchema: z.ZodObject<{
59
59
  max: z.ZodNumber;
60
60
  }, "strip", z.ZodTypeAny, {
61
61
  count: number;
62
+ max: number;
62
63
  mean: number;
63
64
  m2: number;
64
- max: number;
65
65
  }, {
66
66
  count: number;
67
+ max: number;
67
68
  mean: number;
68
69
  m2: number;
69
- max: number;
70
70
  }>;
71
71
  netErrors: z.ZodObject<{
72
72
  count: z.ZodNumber;
@@ -75,14 +75,14 @@ export declare const RouteEnvelopeSchema: z.ZodObject<{
75
75
  max: z.ZodNumber;
76
76
  }, "strip", z.ZodTypeAny, {
77
77
  count: number;
78
+ max: number;
78
79
  mean: number;
79
80
  m2: number;
80
- max: number;
81
81
  }, {
82
82
  count: number;
83
+ max: number;
83
84
  mean: number;
84
85
  m2: number;
85
- max: number;
86
86
  }>;
87
87
  consoleErrors: z.ZodObject<{
88
88
  count: z.ZodNumber;
@@ -91,64 +91,64 @@ export declare const RouteEnvelopeSchema: z.ZodObject<{
91
91
  max: z.ZodNumber;
92
92
  }, "strip", z.ZodTypeAny, {
93
93
  count: number;
94
+ max: number;
94
95
  mean: number;
95
96
  m2: number;
96
- max: number;
97
97
  }, {
98
98
  count: number;
99
+ max: number;
99
100
  mean: number;
100
101
  m2: number;
101
- max: number;
102
102
  }>;
103
103
  }, "strip", z.ZodTypeAny, {
104
104
  durationMs: {
105
105
  count: number;
106
+ max: number;
106
107
  mean: number;
107
108
  m2: number;
108
- max: number;
109
109
  };
110
110
  net: {
111
111
  count: number;
112
+ max: number;
112
113
  mean: number;
113
114
  m2: number;
114
- max: number;
115
115
  };
116
116
  consoleErrors: {
117
117
  count: number;
118
+ max: number;
118
119
  mean: number;
119
120
  m2: number;
120
- max: number;
121
121
  };
122
122
  netErrors: {
123
123
  count: number;
124
+ max: number;
124
125
  mean: number;
125
126
  m2: number;
126
- max: number;
127
127
  };
128
128
  }, {
129
129
  durationMs: {
130
130
  count: number;
131
+ max: number;
131
132
  mean: number;
132
133
  m2: number;
133
- max: number;
134
134
  };
135
135
  net: {
136
136
  count: number;
137
+ max: number;
137
138
  mean: number;
138
139
  m2: number;
139
- max: number;
140
140
  };
141
141
  consoleErrors: {
142
142
  count: number;
143
+ max: number;
143
144
  mean: number;
144
145
  m2: number;
145
- max: number;
146
146
  };
147
147
  netErrors: {
148
148
  count: number;
149
+ max: number;
149
150
  mean: number;
150
151
  m2: number;
151
- max: number;
152
152
  };
153
153
  }>;
154
154
  }, "strip", z.ZodTypeAny, {
@@ -157,27 +157,27 @@ export declare const RouteEnvelopeSchema: z.ZodObject<{
157
157
  stats: {
158
158
  durationMs: {
159
159
  count: number;
160
+ max: number;
160
161
  mean: number;
161
162
  m2: number;
162
- max: number;
163
163
  };
164
164
  net: {
165
165
  count: number;
166
+ max: number;
166
167
  mean: number;
167
168
  m2: number;
168
- max: number;
169
169
  };
170
170
  consoleErrors: {
171
171
  count: number;
172
+ max: number;
172
173
  mean: number;
173
174
  m2: number;
174
- max: number;
175
175
  };
176
176
  netErrors: {
177
177
  count: number;
178
+ max: number;
178
179
  mean: number;
179
180
  m2: number;
180
- max: number;
181
181
  };
182
182
  };
183
183
  }, {
@@ -186,27 +186,27 @@ export declare const RouteEnvelopeSchema: z.ZodObject<{
186
186
  stats: {
187
187
  durationMs: {
188
188
  count: number;
189
+ max: number;
189
190
  mean: number;
190
191
  m2: number;
191
- max: number;
192
192
  };
193
193
  net: {
194
194
  count: number;
195
+ max: number;
195
196
  mean: number;
196
197
  m2: number;
197
- max: number;
198
198
  };
199
199
  consoleErrors: {
200
200
  count: number;
201
+ max: number;
201
202
  mean: number;
202
203
  m2: number;
203
- max: number;
204
204
  };
205
205
  netErrors: {
206
206
  count: number;
207
+ max: number;
207
208
  mean: number;
208
209
  m2: number;
209
- max: number;
210
210
  };
211
211
  };
212
212
  }>;
@@ -12,6 +12,7 @@
12
12
  */
13
13
  import { type KeyObject } from 'node:crypto';
14
14
  import { z } from 'zod';
15
+ import { LicenseActivation } from '@reticlehq/core';
15
16
  /** Outcome of verifying a license key. */
16
17
  export declare const LicenseStatus: {
17
18
  readonly VALID: "valid";
@@ -21,18 +22,31 @@ export declare const LicenseStatus: {
21
22
  readonly EXPIRED: "expired";
22
23
  };
23
24
  export type LicenseStatus = (typeof LicenseStatus)[keyof typeof LicenseStatus];
24
- /** The signed claims inside a key. `exp` is epoch ms; `features` (optional) scopes which ee features unlock. */
25
+ /**
26
+ * The signed claims inside a key. `exp` is epoch ms; `features` (optional) scopes which ee features
27
+ * unlock.
28
+ *
29
+ * `lid` is the STABLE LICENSE ID and the only safe join key for per-customer usage: `org` is a display
30
+ * name a human typed at signing time, so two customers called "Acme" merge into one and a customer who
31
+ * renames splits into two. Every key carries one (minted by the issuer, `scripts/issue-license.mjs`),
32
+ * which is why it is required rather than optional — an optional id is one that goes missing on exactly
33
+ * the keys somebody later needs to attribute, and a key without it should be re-issued, not silently
34
+ * counted as nobody.
35
+ */
25
36
  declare const LicensePayloadSchema: z.ZodObject<{
37
+ lid: z.ZodString;
26
38
  org: z.ZodString;
27
39
  plan: z.ZodString;
28
40
  exp: z.ZodNumber;
29
41
  features: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
30
42
  }, "strip", z.ZodTypeAny, {
43
+ lid: string;
31
44
  org: string;
32
45
  plan: string;
33
46
  exp: number;
34
47
  features?: string[] | undefined;
35
48
  }, {
49
+ lid: string;
36
50
  org: string;
37
51
  plan: string;
38
52
  exp: number;
@@ -42,13 +56,44 @@ export type LicensePayload = z.infer<typeof LicensePayloadSchema>;
42
56
  type LicenseCheck = {
43
57
  status: typeof LicenseStatus.VALID;
44
58
  payload: LicensePayload;
59
+ }
60
+ /**
61
+ * EXPIRED carries the payload; the other failures cannot. The line is the SIGNATURE, not the
62
+ * clock: an expired key was genuinely issued by us and its claims are exactly as trustworthy as
63
+ * they were yesterday, so the licence id is safe to use. A malformed or wrongly-signed key has no
64
+ * verified claims at all, and naming a customer from one would mean trusting whatever was pasted.
65
+ */
66
+ | {
67
+ status: typeof LicenseStatus.EXPIRED;
68
+ payload: LicensePayload;
45
69
  } | {
46
- status: Exclude<LicenseStatus, typeof LicenseStatus.VALID>;
70
+ status: Exclude<LicenseStatus, typeof LicenseStatus.VALID | typeof LicenseStatus.EXPIRED>;
47
71
  };
48
72
  /** Sign a payload into a license key — the ISSUER side (Reticle's private key). Exposed for the issuer tool + tests. */
49
73
  export declare function signLicenseKey(payload: LicensePayload, privateKey: KeyObject): string;
50
74
  /** Verify a key against the issuer public key. Never throws — returns a structured status. */
51
75
  export declare function verifyLicenseKey(key: string | undefined, publicKey: KeyObject, now: number): LicenseCheck;
76
+ /**
77
+ * Where to get a key. A named constant because it appears in the gate's error AND in what
78
+ * `reticle license` prints, and a contact address that drifts between the two is a support request
79
+ * that reaches nobody.
80
+ */
81
+ export declare const LICENSE_CONTACT = "hey@reticle.sh";
82
+ /**
83
+ * The features a licence gates in THIS build. The single source: `assertEnterprise` callers name a
84
+ * member rather than a string literal, and `reticle license` reports the list, so what the CLI claims
85
+ * is gated is derived from what is actually gated rather than a second list somebody maintains.
86
+ *
87
+ * It is deliberately short. Reticle's roadmap for enterprise is longer (SSO, SCIM, RBAC, multi-org,
88
+ * policy gates), and none of it is here yet — printing a roadmap as though it were shipped is how a
89
+ * buyer discovers on day two that the thing they paid for does not exist. What is listed is what the
90
+ * running build will actually refuse without a key.
91
+ */
92
+ export declare const EnterpriseFeature: {
93
+ /** Recording an audit event. */
94
+ readonly AUDIT_LOG: "audit-log";
95
+ };
96
+ export type EnterpriseFeature = (typeof EnterpriseFeature)[keyof typeof EnterpriseFeature];
52
97
  /** Thrown when a production-mode enterprise feature is used without a valid license. */
53
98
  export declare class EnterpriseLicenseError extends Error {
54
99
  readonly feature: string;
@@ -66,13 +111,39 @@ export interface GateContext {
66
111
  * Gate an enterprise feature. No-op in dev/eval (requireLicense:false). In production it throws
67
112
  * EnterpriseLicenseError unless a valid, unexpired key that covers `feature` is present.
68
113
  */
69
- export declare function assertEnterprise(feature: string, ctx: GateContext): void;
114
+ export declare function assertEnterprise(feature: string, ctx: GateContext, env?: NodeJS.ProcessEnv, baked?: string): void;
70
115
  /** Env names that carry the activation: the operator's key, and the issuer public key baked at release. */
71
116
  export declare const LICENSE_KEY_ENV = "RETICLE_LICENSE_KEY";
72
117
  export declare const LICENSE_PUBLIC_KEY_ENV = "RETICLE_LICENSE_PUBLIC_KEY";
73
118
  /** The human-facing state of enterprise activation on this machine (what `reticle license status` shows). */
74
- interface LicenseReport {
75
- status: 'active' | 'missing' | 'invalid' | 'expired' | 'eval';
119
+ export interface LicenseReport {
120
+ /**
121
+ * Typed against core's closed list rather than re-listing the strings here: this status is reported
122
+ * on every telemetry event, so a member added in one place and not the other is a silently
123
+ * miscounted column (telemetry contract, rule 4).
124
+ */
125
+ status: LicenseActivation;
126
+ /** The stable license id — what usage is attributed to. Present only when `status` is `active`. */
127
+ licenseId?: string;
128
+ /**
129
+ * What a licence unlocks in THIS build, and where to get one. Always reported, including on an
130
+ * unlicensed install: `reticle license` was the one command that talks about licensing and it named
131
+ * neither, so it told an interested reader to set a variable without saying what it would unlock or
132
+ * how to obtain it. A dead end at exactly the moment somebody is asking.
133
+ */
134
+ /**
135
+ * What a licence unlocks in this build. Present on every branch a PROSPECTIVE customer lands on,
136
+ * because being told to set a variable with no idea what it unlocks is the dead end this exists to
137
+ * close. Absent once a licence is `active`: that reader has already bought, so naming features at
138
+ * them reveals the product surface and answers nothing they asked.
139
+ */
140
+ gated?: readonly string[];
141
+ contact: string;
142
+ /**
143
+ * The key is valid, and scoped to features this build does not gate, so it unlocks nothing here.
144
+ * Present only when true: a `false` on every healthy licence is noise on the common case.
145
+ */
146
+ coversNothingHere?: boolean;
76
147
  org?: string;
77
148
  plan?: string;
78
149
  expiresAt?: number;