explorbot 0.2.4 → 0.3.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 (113) hide show
  1. package/bin/explorbot-cli.ts +19 -7
  2. package/boat/api-tester/src/cli.ts +17 -0
  3. package/boat/doc-collector/src/cli.ts +14 -1
  4. package/boat/prima/README.md +96 -0
  5. package/boat/prima/package.json +14 -10
  6. package/boat/prima/src/cli.ts +29 -12
  7. package/boat/prima/src/envelope.ts +35 -13
  8. package/boat/prima/src/prima.ts +78 -45
  9. package/dist/bin/explorbot-cli.js +19 -7
  10. package/dist/boat/api-tester/src/cli.js +17 -0
  11. package/dist/boat/doc-collector/src/cli.js +14 -1
  12. package/dist/boat/prima/src/cli.js +26 -7
  13. package/dist/boat/prima/src/envelope.js +32 -8
  14. package/dist/boat/prima/src/prima.js +75 -43
  15. package/dist/models.json +4 -4
  16. package/dist/package.json +6 -2
  17. package/dist/src/action-result.d.ts +13 -0
  18. package/dist/src/action-result.js +46 -15
  19. package/dist/src/action.d.ts +5 -2
  20. package/dist/src/action.js +53 -18
  21. package/dist/src/ai/captain/web-mode.js +1 -2
  22. package/dist/src/ai/captain.d.ts +20 -0
  23. package/dist/src/ai/captain.js +10 -1
  24. package/dist/src/ai/driller.js +6 -2
  25. package/dist/src/ai/fisherman-tools.d.ts +40 -1
  26. package/dist/src/ai/fisherman-tools.js +39 -0
  27. package/dist/src/ai/fisherman.js +2 -1
  28. package/dist/src/ai/navigator.d.ts +28 -0
  29. package/dist/src/ai/navigator.js +223 -175
  30. package/dist/src/ai/pilot.d.ts +7 -4
  31. package/dist/src/ai/pilot.js +89 -30
  32. package/dist/src/ai/planner/subpages.js +2 -16
  33. package/dist/src/ai/planner.js +1 -1
  34. package/dist/src/ai/provider.d.ts +2 -2
  35. package/dist/src/ai/provider.js +28 -22
  36. package/dist/src/ai/researcher/cache.d.ts +10 -3
  37. package/dist/src/ai/researcher/cache.js +23 -10
  38. package/dist/src/ai/researcher/deep-analysis.js +1 -1
  39. package/dist/src/ai/researcher/fingerprint-worker.js +21 -4
  40. package/dist/src/ai/researcher.js +6 -4
  41. package/dist/src/ai/rules.js +1 -5
  42. package/dist/src/ai/session-analyst.js +2 -0
  43. package/dist/src/ai/tester.d.ts +6 -3
  44. package/dist/src/ai/tester.js +30 -35
  45. package/dist/src/ai/tools.d.ts +8 -5
  46. package/dist/src/ai/tools.js +83 -57
  47. package/dist/src/commands/config-command.d.ts +51 -0
  48. package/dist/src/commands/config-command.js +117 -0
  49. package/dist/src/commands/index.js +2 -0
  50. package/dist/src/commands/init-command.js +13 -20
  51. package/dist/src/config.d.ts +8 -1
  52. package/dist/src/config.js +43 -1
  53. package/dist/src/experience-tracker.d.ts +2 -0
  54. package/dist/src/experience-tracker.js +12 -0
  55. package/dist/src/explorbot.js +5 -2
  56. package/dist/src/playwright-recorder.js +6 -12
  57. package/dist/src/remote.d.ts +3 -2
  58. package/dist/src/remote.js +8 -2
  59. package/dist/src/state-manager.d.ts +1 -1
  60. package/dist/src/state-manager.js +3 -1
  61. package/dist/src/test-plan.d.ts +9 -0
  62. package/dist/src/test-plan.js +30 -0
  63. package/dist/src/utils/html-diff.d.ts +5 -0
  64. package/dist/src/utils/html-diff.js +65 -6
  65. package/dist/src/utils/logger.d.ts +1 -1
  66. package/dist/src/utils/logger.js +8 -0
  67. package/dist/src/utils/strings.d.ts +2 -0
  68. package/dist/src/utils/strings.js +32 -0
  69. package/dist/src/utils/url-matcher.d.ts +1 -0
  70. package/dist/src/utils/url-matcher.js +31 -2
  71. package/docs/basics/getting-started.md +33 -10
  72. package/docs/basics/providers.md +6 -4
  73. package/docs/contributing/npm-package.md +73 -4
  74. package/docs/index.json +2 -1
  75. package/docs/reference/commands.md +3 -0
  76. package/docs/reference/websocket.md +50 -0
  77. package/docs/superpowers/specs/2026-08-18-prima-false-verdicts.md +159 -0
  78. package/models.json +4 -4
  79. package/package.json +6 -2
  80. package/src/action-result.ts +61 -16
  81. package/src/action.ts +56 -18
  82. package/src/ai/captain/web-mode.ts +1 -2
  83. package/src/ai/captain.ts +9 -1
  84. package/src/ai/driller.ts +6 -2
  85. package/src/ai/fisherman-tools.ts +35 -0
  86. package/src/ai/fisherman.ts +2 -1
  87. package/src/ai/navigator.ts +238 -179
  88. package/src/ai/pilot.ts +104 -36
  89. package/src/ai/planner/subpages.ts +2 -13
  90. package/src/ai/planner.ts +1 -1
  91. package/src/ai/provider.ts +29 -21
  92. package/src/ai/researcher/cache.ts +29 -11
  93. package/src/ai/researcher/deep-analysis.ts +1 -1
  94. package/src/ai/researcher/fingerprint-worker.ts +23 -5
  95. package/src/ai/researcher.ts +6 -4
  96. package/src/ai/rules.ts +1 -5
  97. package/src/ai/session-analyst.ts +2 -0
  98. package/src/ai/tester.ts +33 -34
  99. package/src/ai/tools.ts +88 -61
  100. package/src/commands/config-command.ts +146 -0
  101. package/src/commands/index.ts +2 -0
  102. package/src/commands/init-command.ts +14 -20
  103. package/src/config.ts +47 -2
  104. package/src/experience-tracker.ts +13 -0
  105. package/src/explorbot.ts +4 -2
  106. package/src/playwright-recorder.ts +6 -11
  107. package/src/remote.ts +8 -2
  108. package/src/state-manager.ts +5 -2
  109. package/src/test-plan.ts +38 -0
  110. package/src/utils/html-diff.ts +72 -7
  111. package/src/utils/logger.ts +9 -1
  112. package/src/utils/strings.ts +36 -0
  113. package/src/utils/url-matcher.ts +27 -2
@@ -15,7 +15,7 @@ import { remote } from '../src/remote.js';
15
15
  import { Stats } from '../src/stats.js';
16
16
  import { Plan } from '../src/test-plan.js';
17
17
  import { getCliName } from '../src/utils/cli-name.ts';
18
- import { log, setPreserveConsoleLogs } from '../src/utils/logger.js';
18
+ import { isVerboseMode, log, setPreserveConsoleLogs, setQuietMode } from '../src/utils/logger.js';
19
19
  import { jsonToTable } from '../src/utils/markdown-parser.js';
20
20
  import { parseMarkdownToTerminal } from '../src/utils/markdown-terminal.js';
21
21
  import { type NextStepSection, printNextSteps, relativeToCwd } from '../src/utils/next-steps.ts';
@@ -436,6 +436,23 @@ addCommonOptions(
436
436
  await cmd.execute(args);
437
437
  });
438
438
 
439
+ program
440
+ .command('config [url]')
441
+ .description('Show models, config file and paths used by this run')
442
+ .option('-c, --config <path>', 'Path to configuration file')
443
+ .option('-p, --path <path>', 'Working directory path')
444
+ .option('--json', 'Print the resolved config as JSON')
445
+ .action(async (url, options) => {
446
+ setQuietMode(!isVerboseMode());
447
+ const { ConfigCommand } = await import('../src/commands/config-command.js');
448
+ try {
449
+ console.log(await ConfigCommand.summary({ config: options.config, path: options.path, url, json: options.json }));
450
+ } catch (error) {
451
+ console.error(error instanceof Error ? error.message : 'Unknown error');
452
+ process.exit(1);
453
+ }
454
+ });
455
+
439
456
  program
440
457
  .command('init')
441
458
  .description('Initialize configuration for a project or for this machine')
@@ -912,11 +929,6 @@ ${rows}
912
929
  `;
913
930
  };
914
931
 
915
- const addEnvHelp = (cmd: Command) => {
916
- cmd.addHelpText('after', envHelp);
917
- for (const sub of cmd.commands) addEnvHelp(sub);
918
- };
919
-
920
- addEnvHelp(program);
932
+ program.addHelpText('after', envHelp);
921
933
 
922
934
  program.parse();
@@ -1,9 +1,12 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { Command } from 'commander';
4
+ import { ConfigCommand } from '../../../src/commands/config-command.ts';
5
+ import { listSites } from '../../../src/global-config.ts';
4
6
  import { setPreserveConsoleLogs } from '../../../src/utils/logger.ts';
5
7
  import { getStyles } from './ai/chief/styles.ts';
6
8
  import { ApiBot, type ApibotOptions } from './apibot.ts';
9
+ import { ApibotConfigParser } from './config.ts';
7
10
 
8
11
  function buildOptions(options: any): ApibotOptions {
9
12
  return {
@@ -82,6 +85,20 @@ export function createApiCommands(name = 'api'): Command {
82
85
  }
83
86
  });
84
87
 
88
+ addCommonOptions(cmd.command('config [endpoint]').description('Show models, config file and paths used by this run'))
89
+ .option('--json', 'Print the resolved config as JSON')
90
+ .action(async (endpoint, options) => {
91
+ const parser = ApibotConfigParser.getInstance();
92
+ const [site] = listSites();
93
+ try {
94
+ const config = await parser.loadConfig({ config: options.config, path: options.path, endpoint: endpoint || site?.url });
95
+ console.log(ConfigCommand.render(config, { configPath: parser.getConfigPath(), root: parser.getProjectRoot(), json: options.json }));
96
+ } catch (error) {
97
+ console.error(error instanceof Error ? error.message : 'Unknown error');
98
+ process.exit(1);
99
+ }
100
+ });
101
+
85
102
  addCommonOptions(cmd.command('test <planfile> [index]').description('Execute tests from a plan file. Index: 1, 1-3, *')).action(async (planfile, index, options) => {
86
103
  setPreserveConsoleLogs(true);
87
104
  try {
@@ -1,7 +1,8 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { Command } from 'commander';
4
- import { setPreserveConsoleLogs } from '../../../src/utils/logger.ts';
4
+ import { ConfigCommand } from '../../../src/commands/config-command.ts';
5
+ import { isVerboseMode, setPreserveConsoleLogs, setQuietMode } from '../../../src/utils/logger.ts';
5
6
  import { DocBot, type DocbotOptions } from './docbot.ts';
6
7
 
7
8
  function buildOptions(options: any): DocbotOptions {
@@ -65,6 +66,18 @@ export function createDocsCommands(name = 'docs'): Command {
65
66
  }
66
67
  });
67
68
 
69
+ addCommonOptions(cmd.command('config [url]').description('Show models, config file and paths used by this run'))
70
+ .option('--json', 'Print the resolved config as JSON')
71
+ .action(async (url, options) => {
72
+ setQuietMode(!isVerboseMode());
73
+ try {
74
+ console.log(await ConfigCommand.summary({ config: options.config, path: options.path, url, json: options.json }));
75
+ } catch (error) {
76
+ console.error(error instanceof Error ? error.message : 'Unknown error');
77
+ process.exit(1);
78
+ }
79
+ });
80
+
68
81
  cmd
69
82
  .command('init')
70
83
  .description('Initialize doc collector configuration')
@@ -0,0 +1,96 @@
1
+ # prima-cli
2
+
3
+ Prima is a high-level AI browser driver. You describe the behaviour you want checked, and it works out how to reach it — instead of writing selectors and step-by-step click paths.
4
+
5
+ It drives a browser opened by [playwright-cli](https://www.npmjs.com/package/playwright-cli), or one of its own.
6
+
7
+ ```bash
8
+ npx prima-cli check "a workflow can be created and appears in the list" --expected "the new workflow is listed"
9
+ ```
10
+
11
+ ## Install
12
+
13
+ ```bash
14
+ npx prima-cli --help # no install
15
+ npm install -g prima-cli # or install it
16
+ ```
17
+
18
+ Prima also ships inside [explorbot](https://www.npmjs.com/package/explorbot), so `npx explorbot prima <command>` runs the same tool if you already have it.
19
+
20
+ Requires Node.js 24+. Playwright browsers come from `npx playwright install chromium`.
21
+
22
+ ## Session
23
+
24
+ Prima needs a browser to drive. Either attach to a `playwright-cli` session:
25
+
26
+ ```bash
27
+ playwright-cli open https://app.example.com
28
+ prima-cli check "the settings page saves a changed theme"
29
+ playwright-cli close
30
+ ```
31
+
32
+ Or let prima own the browser:
33
+
34
+ ```bash
35
+ prima-cli browser start --url https://app.example.com
36
+ prima-cli do "open the account menu" "choose the settings entry"
37
+ prima-cli browser stop
38
+ ```
39
+
40
+ ## Commands
41
+
42
+ | Command | What it does |
43
+ | --- | --- |
44
+ | `check <scenario>` | Run a scenario end to end as a test, verify it, and report the steps it took |
45
+ | `do <instructions...>` | Run high-level instructions tester-style, one argument per instruction |
46
+ | `verify <assertion>` | Assert a statement about the current page |
47
+ | `ask <question>` | Answer a question about the current page |
48
+ | `research` | Map the current page and return verified locators |
49
+ | `go <target>` | Navigate to a url, a path, or a page described in plain words |
50
+ | `pw <fn>` | Run a Playwright function expression against the open page |
51
+ | `status <hash>` | Show the artifacts and page detail recorded for an earlier command |
52
+ | `report` | Turn every command of a session into one html and markdown report |
53
+ | `browser start\|stop\|status\|list` | Manage the browsers prima drives |
54
+ | `config` | Show models, config file and paths used by this run |
55
+
56
+ `check` takes an outcome rather than a click path. It runs on the page you are already on and never reloads it, so an open dialog survives the check. Each `--expected` outcome comes back as PASSED, FAILED, CONTRADICTION or not verified — settled against a screenshot of the whole page, because what a user can see is the proof.
57
+
58
+ `do` accounts for every instruction you gave: each is reported as ok, FAIL or ??. Nothing runs past the last instruction.
59
+
60
+ ## AI model
61
+
62
+ Prima needs a model, and takes it from the environment. There is no `init` to run and nothing is written for you.
63
+
64
+ ```bash
65
+ export PRIMA_CLI_AI_MODEL=openrouter/openai/gpt-oss-120b
66
+ export OPENROUTER_API_KEY=your-key
67
+ ```
68
+
69
+ The provider comes from the model name, so that is the whole setup. Fish uses `set -gx` instead of `export`. Since prima is usually run by a coding agent, the better home is the agent's own config — in `~/.claude/settings.json`, an `env` block reaches every command the agent runs:
70
+
71
+ ```json
72
+ {
73
+ "env": {
74
+ "PRIMA_CLI_AI_MODEL": "openrouter/openai/gpt-oss-120b",
75
+ "OPENROUTER_API_KEY": "your-key"
76
+ }
77
+ }
78
+ ```
79
+
80
+ Screenshot analysis needs its own model — one is never guessed from the main model. Set `PRIMA_CLI_VISION_MODEL`, or pass `--vision-model` for a single run, as `--model` does for the main one. Without it prima still runs, but settles `check` outcomes from the run log rather than from the page as seen.
81
+
82
+ Every `PRIMA_CLI_*` variable mirrors the `EXPLORBOT_*` one of the same name and wins over it, so prima can be pointed at a different provider, model or URL without disturbing an explorbot setup on the same machine. `PRIMA_CLI_AI_MODEL`, `PRIMA_CLI_URL`, `PRIMA_CLI_VISION_MODEL` and the rest all work this way.
83
+
84
+ Where [explorbot](https://www.npmjs.com/package/explorbot) is already configured, prima reads its config too — a project's `explorbot.config.js` first, then `~/.explorbot/config.js`. Variables win over both. `prima config` prints what a directory resolves to, and `--ephemeral` keeps no state between runs, writing output to a temp directory.
85
+
86
+ Providers: openai, anthropic, google, groq, mistral, openrouter, sambanova.
87
+
88
+ Without a model `pw` still works, since it runs Playwright directly.
89
+
90
+ ## Agents
91
+
92
+ Prima is built to be called by a coding agent: one call takes a whole job, and the reply is a compact report rather than a browser transcript. Point your agent at `prima-cli --help` and it will find its way.
93
+
94
+ ## License
95
+
96
+ Elastic-2.0. Part of [explorbot](https://github.com/testomatio/explorbot).
@@ -1,16 +1,20 @@
1
1
  {
2
- "name": "prima",
3
- "version": "1.0.0",
2
+ "name": "prima-cli",
3
+ "version": "0.0.0",
4
4
  "description": "High-level browser driver CLI for orchestrating agents",
5
+ "license": "Elastic-2.0",
5
6
  "type": "module",
6
- "bin": { "prima": "./bin/prima-cli.ts" },
7
- "scripts": {
8
- "format": "biome format --write .",
9
- "lint:fix": "biome lint --write .",
10
- "check:fix": "biome check --write ."
7
+ "bin": {
8
+ "prima-cli": "./dist/boat/prima/bin/prima-cli.js"
11
9
  },
12
- "dependencies": {
13
- "commander": "^14.0.1",
14
- "dedent": "^1.6.0"
10
+ "files": ["dist/"],
11
+ "keywords": ["cli", "playwright", "browser", "ai", "agent", "testing"],
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "https://github.com/testomatio/explorbot",
15
+ "directory": "boat/prima"
16
+ },
17
+ "engines": {
18
+ "node": ">=24.0.0"
15
19
  }
16
20
  }
@@ -22,17 +22,27 @@ const helpContract = dedent`
22
22
  `;
23
23
 
24
24
  const checkHelp = dedent`
25
- check takes an outcome rather than a click path, and works out how to reach it.
25
+ check takes an outcome rather than a click path, and works out how to reach it. It runs
26
+ on the page you are already on and never reloads it, so an open dialog survives the check.
26
27
  --expected one outcome the run must reach, repeatable for several. Without it the
27
28
  scenario text is the single expected outcome. Each comes back under
28
- ### Expected outcomes as PASSED, FAILED or not verified - "not verified"
29
- means the run never checked it, which is not the same as false.
29
+ ### Expected outcomes as PASSED, FAILED, CONTRADICTION or not verified.
30
+ "not verified" means the run never checked it, which is not the same
31
+ as false.
32
+ Outcomes are settled against a screenshot of the whole page: what a user can see is
33
+ the proof, and the run log only says what was done. CONTRADICTION means the two
34
+ disagree - reported with both sides rather than settled one way, and ### Artifacts
35
+ then names the html, aria and screenshot on disk so you can judge it yourself. Not
36
+ finding something in the picture is not enough on its own; that is "not verified".
37
+ ok: follows those outcomes - false when one FAILED or CONTRADICTED, or when the run
38
+ could not complete, which is reported as such rather than as an app failure.
30
39
  Page problems seen on the way appear under ### Answer, not as step failures.
31
40
  `;
32
41
 
33
42
  const doHelp = dedent`
34
- Each instruction is numbered and accounted for: ### Steps reports each as ok or FAIL
35
- with what proved it. One that could not be carried out fails the command and says why.
43
+ Each instruction is numbered and accounted for: ### Steps reports each as ok, FAIL or ??.
44
+ ?? means the action ran but the run ended without confirming that instruction - read the
45
+ steps above it. Only FAIL and an instruction the page could not carry out fail the command.
36
46
  Nothing runs past the last instruction given. A whole remaining sequence in one call is
37
47
  what makes this tier cheap.
38
48
  `;
@@ -94,6 +104,8 @@ function addCommonOptions(cmd: Command): Command {
94
104
  .option('-p, --path <path>', 'Working directory path')
95
105
  .option('-i, --instance <name>', 'Browser instance to drive')
96
106
  .option('--session [file]', 'Persist cookies and storage to a session file')
107
+ .option('--model <model>', 'Main model, as provider/model-id')
108
+ .option('--vision-model <model>', 'Model for screenshot analysis, as provider/model-id')
97
109
  .option('--ephemeral', 'Keep no state between runs; applies to config-free runs, where output goes to a temp directory')
98
110
  .option('--framework <name>', 'Not active yet: framework the reported code targets, codeceptjs or playwright')
99
111
  .option('--url <url>', 'Page to open when the session has no page yet')
@@ -103,7 +115,10 @@ function addCommonOptions(cmd: Command): Command {
103
115
  }
104
116
 
105
117
  function primaFor(options: any): Prima {
118
+ Prima.applyEnv();
106
119
  if (options.ephemeral) process.env.EXPLORBOT_EPHEMERAL = '1';
120
+ if (options.model) process.env.EXPLORBOT_AI_MODEL = options.model;
121
+ if (options.visionModel) process.env.EXPLORBOT_VISION_MODEL = options.visionModel;
107
122
  return new Prima(buildOptions(options));
108
123
  }
109
124
 
@@ -186,13 +201,15 @@ export function createPrimaCommands(name = 'prima'): Command {
186
201
  await runPrima(options, `go ${target}`, (prima) => prima.go(target));
187
202
  });
188
203
 
189
- addCommonOptions(cmd.command('config').description('Show the AI models prima runs on and the config file they come from')).action(async (options) => {
190
- setQuietMode(!isVerboseMode());
191
- const prima = primaFor(options);
192
- console.log(await prima.config().catch((error: unknown) => browserErrorMessage(error)));
193
- await prima.stop().catch(() => {});
194
- process.exit(0);
195
- });
204
+ addCommonOptions(cmd.command('config').description('Show models, config file and paths used by this run'))
205
+ .option('--json', 'Print the resolved config as JSON')
206
+ .action(async (options) => {
207
+ setQuietMode(!isVerboseMode());
208
+ const prima = primaFor(options);
209
+ console.log(await prima.config(options.json).catch((error: unknown) => browserErrorMessage(error)));
210
+ await prima.stop().catch(() => {});
211
+ process.exit(0);
212
+ });
196
213
 
197
214
  addCommonOptions(cmd.command('status <hash>').description('Show the artifacts and page detail recorded for an earlier command')).action(async (hash, options) => {
198
215
  await runPrima(options, `status ${hash}`, (prima) => prima.status(hash), false);
@@ -2,9 +2,10 @@ import { mkdirSync, writeFileSync } from 'node:fs';
2
2
  import path from 'node:path';
3
3
 
4
4
  const EXPECTATION_LABELS = {
5
- passed: 'PASSED ',
6
- failed: 'FAILED ',
7
- unverified: 'not verified',
5
+ passed: 'PASSED ',
6
+ failed: 'FAILED ',
7
+ unverified: 'not verified ',
8
+ contradiction: 'CONTRADICTION',
8
9
  };
9
10
 
10
11
  export interface InstanceInfo {
@@ -21,8 +22,9 @@ export interface EnvelopeData {
21
22
  used?: string[];
22
23
  page: { url: string; previousUrl?: string; title: string; state: string; visits: number };
23
24
  changes?: string | null;
24
- steps?: Array<{ label: string; ok: boolean; proof: string }>;
25
- expectations?: Array<{ text: string; status: 'passed' | 'failed' | 'unverified' }>;
25
+ steps?: Array<{ label: string; ok: boolean; unconfirmed?: boolean; proof: string }>;
26
+ expectations?: Array<{ text: string; status: 'passed' | 'failed' | 'unverified' | 'contradiction'; evidence?: string }>;
27
+ warning?: string;
26
28
  stepFiles?: string;
27
29
  value?: string;
28
30
  answer?: string;
@@ -31,23 +33,28 @@ export interface EnvelopeData {
31
33
  failure?: { error: string; compactAria?: string };
32
34
  instance: InstanceInfo;
33
35
  status?: string;
34
- artifacts?: { aria: string; html: string; network?: string };
36
+ artifacts?: { aria: string; html: string; screenshot?: string; network?: string };
35
37
  }
36
38
 
37
39
  export function renderEnvelope(data: EnvelopeData): string {
38
- const sections = [renderResult(data), renderPage(data), renderValue(data), renderChanges(data), renderSteps(data), renderExpectations(data), renderOutcome(data), ...renderFailure(data), renderInstance(data), renderArtifacts(data)];
40
+ const sections = [renderResult(data), renderPage(data), renderValue(data), renderChanges(data), renderSteps(data), renderExpectations(data), renderWarning(data), renderOutcome(data), ...renderFailure(data), renderInstance(data), renderArtifacts(data)];
39
41
  return sections.filter((section) => section).join('\n\n');
40
42
  }
41
43
 
42
- export function writeArtifacts(dir: string, snapshot: { aria: string | null; html: string | null; requests: unknown[] }): { aria: string; html: string; network?: string } {
44
+ export function writeArtifacts(dir: string, snapshot: { aria: string | null; html: string | null; screenshot?: Buffer; requests: unknown[] }): { aria: string; html: string; screenshot?: string; network?: string } {
43
45
  mkdirSync(dir, { recursive: true });
44
- const paths: { aria: string; html: string; network?: string } = {
46
+ const paths: { aria: string; html: string; screenshot?: string; network?: string } = {
45
47
  aria: path.resolve(dir, 'aria.yml'),
46
48
  html: path.resolve(dir, 'page.html'),
47
49
  };
48
50
  writeFileSync(paths.aria, snapshot.aria ?? '', 'utf-8');
49
51
  writeFileSync(paths.html, snapshot.html ?? '', 'utf-8');
50
52
 
53
+ if (snapshot.screenshot) {
54
+ paths.screenshot = path.resolve(dir, 'page.png');
55
+ writeFileSync(paths.screenshot, snapshot.screenshot);
56
+ }
57
+
51
58
  if (!snapshot.requests.length) return paths;
52
59
 
53
60
  paths.network = path.resolve(dir, 'network.jsonl');
@@ -87,7 +94,10 @@ function renderSteps(data: EnvelopeData): string | null {
87
94
 
88
95
  const lines: string[] = [];
89
96
  data.steps.forEach((step, index) => {
90
- lines.push(`${index + 1}. ${step.ok ? 'ok ' : 'FAIL'} ${step.label}`);
97
+ let mark = 'FAIL';
98
+ if (step.ok) mark = 'ok ';
99
+ if (step.unconfirmed) mark = '?? ';
100
+ lines.push(`${index + 1}. ${mark} ${step.label}`);
91
101
  for (const line of (step.proof || '').split('\n').filter(Boolean)) lines.push(` ${line}`);
92
102
  });
93
103
  if (data.stepFiles) lines.push('', `page after each step: ${data.stepFiles}`);
@@ -96,10 +106,21 @@ function renderSteps(data: EnvelopeData): string | null {
96
106
 
97
107
  function renderExpectations(data: EnvelopeData): string | null {
98
108
  if (!data.expectations?.length) return null;
99
- const lines = data.expectations.map((expectation, index) => `${index + 1}. ${EXPECTATION_LABELS[expectation.status]} ${expectation.text}`);
109
+
110
+ const lines: string[] = [];
111
+ data.expectations.forEach((expectation, index) => {
112
+ lines.push(`${index + 1}. ${EXPECTATION_LABELS[expectation.status]} ${expectation.text}`);
113
+ if (expectation.status !== 'contradiction' && expectation.status !== 'failed') return;
114
+ for (const line of (expectation.evidence || '').split('\n').filter(Boolean)) lines.push(` ${line}`);
115
+ });
100
116
  return section('Expected outcomes', lines.join('\n'));
101
117
  }
102
118
 
119
+ function renderWarning(data: EnvelopeData): string | null {
120
+ if (!data.warning) return null;
121
+ return section('Warning', data.warning);
122
+ }
123
+
103
124
  function renderOutcome(data: EnvelopeData): string | null {
104
125
  if (data.answer) return section('Answer', data.answer);
105
126
  if (data.research) return section('Research', data.research);
@@ -154,8 +175,9 @@ function tabsLabel(tabs: number): string {
154
175
 
155
176
  export function renderArtifacts(data: EnvelopeData): string | null {
156
177
  if (!data.artifacts) return null;
157
- const lines = [`aria: ${data.artifacts.aria}`, `html: ${data.artifacts.html}`];
158
- if (data.artifacts.network) lines.push(`network: ${data.artifacts.network}`);
178
+ const lines = [`aria: ${data.artifacts.aria}`, `html: ${data.artifacts.html}`];
179
+ if (data.artifacts.screenshot) lines.push(`screenshot: ${data.artifacts.screenshot}`);
180
+ if (data.artifacts.network) lines.push(`network: ${data.artifacts.network}`);
159
181
  return section('Artifacts', lines.join('\n'));
160
182
  }
161
183