explorbot 0.2.3 → 0.2.4

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 (156) hide show
  1. package/README.md +1 -1
  2. package/bin/explorbot-cli.ts +7 -1
  3. package/boat/api-tester/src/config.ts +4 -2
  4. package/boat/doc-collector/bin/doc-collector-cli.ts +2 -0
  5. package/boat/doc-collector/src/ai/documentarian.ts +61 -31
  6. package/boat/doc-collector/src/config.ts +4 -2
  7. package/boat/prima/bin/prima-cli.ts +0 -0
  8. package/boat/prima/src/activity-line.ts +33 -0
  9. package/boat/prima/src/cli.ts +115 -86
  10. package/boat/prima/src/envelope.ts +80 -52
  11. package/boat/prima/src/prima.ts +547 -128
  12. package/boat/prima/src/pw-parser.ts +11 -1
  13. package/boat/prima/src/pw-registry.ts +4 -5
  14. package/boat/prima/src/session-log.ts +126 -0
  15. package/dist/bin/explorbot-cli.js +7 -1
  16. package/dist/boat/api-tester/bin/apibot-cli.js +2 -0
  17. package/dist/boat/api-tester/src/config.js +4 -2
  18. package/dist/boat/doc-collector/bin/doc-collector-cli.js +2 -0
  19. package/dist/boat/doc-collector/src/ai/documentarian.js +44 -19
  20. package/dist/boat/doc-collector/src/config.js +4 -2
  21. package/dist/boat/prima/src/activity-line.js +30 -0
  22. package/dist/boat/prima/src/cli.js +97 -77
  23. package/dist/boat/prima/src/envelope.js +70 -44
  24. package/dist/boat/prima/src/prima.js +515 -119
  25. package/dist/boat/prima/src/pw-parser.js +13 -1
  26. package/dist/boat/prima/src/pw-registry.js +4 -5
  27. package/dist/boat/prima/src/session-log.js +108 -0
  28. package/dist/package.json +3 -2
  29. package/dist/rules/navigator/verification-actions.md +20 -0
  30. package/dist/src/action-result.d.ts +7 -0
  31. package/dist/src/action-result.js +4 -0
  32. package/dist/src/action.d.ts +2 -0
  33. package/dist/src/action.js +36 -1
  34. package/dist/src/ai/captain/web-mode.js +6 -3
  35. package/dist/src/ai/captain.js +2 -0
  36. package/dist/src/ai/navigator.d.ts +7 -0
  37. package/dist/src/ai/navigator.js +10 -6
  38. package/dist/src/ai/pilot.d.ts +4 -0
  39. package/dist/src/ai/pilot.js +48 -2
  40. package/dist/src/ai/provider.js +2 -2
  41. package/dist/src/ai/rerunner.js +2 -1
  42. package/dist/src/ai/researcher.js +1 -1
  43. package/dist/src/ai/rules.js +17 -10
  44. package/dist/src/ai/task-agent.js +4 -1
  45. package/dist/src/ai/tester.d.ts +1 -1
  46. package/dist/src/ai/tester.js +33 -33
  47. package/dist/src/ai/tools.d.ts +14 -0
  48. package/dist/src/ai/tools.js +113 -36
  49. package/dist/src/config.d.ts +1 -0
  50. package/dist/src/config.js +13 -4
  51. package/dist/src/execution-controller.d.ts +2 -0
  52. package/dist/src/execution-controller.js +6 -0
  53. package/dist/src/explorbot.d.ts +2 -1
  54. package/dist/src/explorbot.js +3 -1
  55. package/dist/src/explorer.js +2 -3
  56. package/dist/src/playwright-recorder.js +30 -0
  57. package/dist/src/remote.d.ts +54 -0
  58. package/dist/src/remote.js +229 -0
  59. package/dist/src/reporter.d.ts +1 -0
  60. package/dist/src/reporter.js +7 -1
  61. package/dist/src/state-manager.d.ts +2 -1
  62. package/dist/src/stats.d.ts +1 -0
  63. package/dist/src/stats.js +1 -0
  64. package/dist/src/test-plan.d.ts +2 -0
  65. package/dist/src/test-plan.js +7 -0
  66. package/dist/src/utils/aria.d.ts +2 -8
  67. package/dist/src/utils/aria.js +69 -40
  68. package/dist/src/utils/html.js +1 -0
  69. package/dist/src/utils/logger.d.ts +6 -0
  70. package/dist/src/utils/logger.js +24 -0
  71. package/dist/src/utils/page-readiness.js +18 -1
  72. package/dist/src/utils/url-matcher.js +3 -0
  73. package/dist/src/utils/web-element.d.ts +2 -0
  74. package/dist/src/utils/web-element.js +8 -0
  75. package/dist/src/utils/web-sandbox.d.ts +1 -1
  76. package/dist/src/utils/web-sandbox.js +2 -3
  77. package/docs/api-testing/basics.md +90 -0
  78. package/docs/api-testing/planning.md +57 -0
  79. package/docs/api-testing/running-tests.md +55 -0
  80. package/docs/assets/cloud-report.png +0 -0
  81. package/docs/assets/html-report.png +0 -0
  82. package/docs/assets/langfuse-trace.png +0 -0
  83. package/docs/assets/successful-explore-run.png +0 -0
  84. package/docs/basics/getting-started.md +140 -0
  85. package/docs/basics/prerequisites.md +63 -0
  86. package/docs/basics/providers.md +362 -0
  87. package/docs/basics/running.md +78 -0
  88. package/docs/contributing/ai-integration-tests.md +57 -0
  89. package/docs/contributing/contributing.md +90 -0
  90. package/docs/contributing/demo-videos.md +36 -0
  91. package/docs/contributing/npm-package.md +138 -0
  92. package/docs/contributing/observability.md +227 -0
  93. package/docs/contributing/regression-tests.md +103 -0
  94. package/docs/contributing/testing.md +95 -0
  95. package/docs/doc-collection/basics.md +128 -0
  96. package/docs/doc-collection/crawling.md +67 -0
  97. package/docs/doc-collection/interactive-mode.md +99 -0
  98. package/docs/index.json +86 -0
  99. package/docs/reference/commands.md +994 -0
  100. package/docs/reference/configuration.md +569 -0
  101. package/docs/reference/scripting.md +303 -0
  102. package/docs/superpowers/plans/2026-08-01-actor-boat.md +925 -0
  103. package/docs/superpowers/plans/2026-08-01-prima-boat.md +1120 -0
  104. package/docs/superpowers/reviews/2026-08-06-prima-vs-playwright-cli.md +268 -0
  105. package/docs/superpowers/specs/2026-08-01-actor-boat-design.md +204 -0
  106. package/docs/superpowers/specs/2026-08-01-prima-boat-design.md +242 -0
  107. package/docs/superpowers/specs/2026-08-03-global-config-design.md +138 -0
  108. package/docs/superpowers/specs/2026-08-07-prima-fixes-design.md +394 -0
  109. package/docs/web-testing/agents.md +158 -0
  110. package/docs/web-testing/automated-tests.md +134 -0
  111. package/docs/web-testing/basics.md +91 -0
  112. package/docs/web-testing/customization.md +131 -0
  113. package/docs/web-testing/hooks.md +238 -0
  114. package/docs/web-testing/page-interaction.md +84 -0
  115. package/docs/web-testing/planner.md +122 -0
  116. package/docs/web-testing/rerun.md +164 -0
  117. package/docs/web-testing/researcher.md +380 -0
  118. package/docs/workflow/agentic-usage.md +233 -0
  119. package/docs/workflow/application-spec.md +73 -0
  120. package/docs/workflow/ci.md +202 -0
  121. package/docs/workflow/knowledge.md +310 -0
  122. package/docs/workflow/planning-styles.md +67 -0
  123. package/docs/workflow/reporting.md +133 -0
  124. package/docs/workflow/test-plans.md +90 -0
  125. package/package.json +3 -2
  126. package/rules/navigator/verification-actions.md +20 -0
  127. package/src/action-result.ts +11 -0
  128. package/src/action.ts +38 -2
  129. package/src/ai/captain/web-mode.ts +6 -3
  130. package/src/ai/captain.ts +3 -0
  131. package/src/ai/navigator.ts +14 -8
  132. package/src/ai/pilot.ts +53 -2
  133. package/src/ai/provider.ts +2 -4
  134. package/src/ai/rerunner.ts +2 -1
  135. package/src/ai/researcher.ts +1 -1
  136. package/src/ai/rules.ts +17 -10
  137. package/src/ai/task-agent.ts +3 -1
  138. package/src/ai/tester.ts +32 -33
  139. package/src/ai/tools.ts +132 -36
  140. package/src/config.ts +15 -4
  141. package/src/execution-controller.ts +8 -0
  142. package/src/explorbot.ts +4 -2
  143. package/src/explorer.ts +2 -2
  144. package/src/playwright-recorder.ts +23 -0
  145. package/src/remote.ts +238 -0
  146. package/src/reporter.ts +7 -1
  147. package/src/state-manager.ts +2 -1
  148. package/src/stats.ts +1 -0
  149. package/src/test-plan.ts +9 -0
  150. package/src/utils/aria.ts +65 -45
  151. package/src/utils/html.ts +1 -0
  152. package/src/utils/logger.ts +24 -1
  153. package/src/utils/page-readiness.ts +24 -1
  154. package/src/utils/url-matcher.ts +3 -0
  155. package/src/utils/web-element.ts +9 -0
  156. package/src/utils/web-sandbox.ts +3 -4
@@ -2,81 +2,63 @@ import { Command } from 'commander';
2
2
  import dedent from 'dedent';
3
3
  import { keepServerRunning } from "../../../src/browser-server.js";
4
4
  import { browserErrorMessage } from "../../../src/utils/browser-errors.js";
5
- import { setPreserveConsoleLogs } from "../../../src/utils/logger.js";
5
+ import { isVerboseMode, setQuietMode } from "../../../src/utils/logger.js";
6
+ import { clearActivityLine, trackActivityLine } from "./activity-line.js";
6
7
  import { renderEnvelope } from "./envelope.js";
7
8
  import { Prima } from "./prima.js";
8
9
  const helpContract = dedent `
9
- Prima drives a browser that is already open. One command per process; every command
10
- prints a plain-text envelope on stdout and exits 0 when ok, 1 when not.
10
+ Prima is a high-level AI extension to playwright-cli, driving the browser it has open.
11
11
 
12
- TIERS - choose by what you hold, not by how hard the step looks
13
- pw <fn> Precise. A Playwright function expression built from a locator you
14
- already verified. No AI on the happy path.
15
- prima pw "({ page }) => page.click('[data-test=submit]')"
16
- click / fill One action described in words; AI resolves it on the current page.
17
- prima click "the primary action button in the header"
18
- prima fill "the search box" "a search term"
19
- do <steps...> Several described steps, run tester-style in one process.
20
- prima do "open the account menu" "choose the settings entry"
21
- Never pass a locator or a function expression to click/fill/do - describe the target.
22
- Never pass a description to pw - it takes executable code only.
12
+ playwright-cli open <url> starts the session
13
+ prima <command> ... drives it
14
+ playwright-cli close ends it
23
15
 
24
- LOOP
25
- prima go <url|path|words> reach the page you want to work on
26
- prima research once per new page; returns verified locators
27
- prima pw "..." drive the page with those locators
28
- prima verify "..." assert the outcome (prima ask "..." to inspect instead)
29
- Fall back to click/fill/do whenever research left you no locator to hold.
16
+ One call takes a whole job:
30
17
 
31
- ENVELOPE
32
- ### Result ok, command, healed, used
33
- ### Page url, title, state hash, visit count
34
- ### Changes what the accessibility tree gained or lost
35
- ### Answer | ### Research | ### Verdict output of ask, research, verify
36
- ### Failure error, reasoning, healing attempts, compact ARIA of the page
37
- ### Instance the browser you are on and the other instances running
38
- ### Artifacts paths to the full aria.yml, page.html and network.jsonl
39
- used: is code that already executed - CodeceptJS steps to copy as they are, except
40
- for pw, whose Playwright expression a test needs inside I.usePlaywrightTo(...).
41
- Log lines can precede the envelope; start parsing at the first ### line.
42
-
43
- HEALING AND FAILURE
44
- A failed action is retried by AI along a different route; healed: true means the
45
- outcome was reached another way and used: holds the code that worked.
46
- --no-heal skips that and fails fast.
47
- Failures print compact ARIA inline, so retarget from the envelope itself and open
48
- the artifact files only when the inline snapshot is not enough.
49
-
50
- SESSIONS
51
- By default prima attaches to the playwright-cli browser of this workspace and works
52
- on the tabs it already has open; driving the same session from both tools is the
53
- intended usage.
54
- playwright-cli open <url> the session prima attaches to
55
- --pw-session <title> which playwright-cli session, when several are open
56
- --endpoint <ep> attach to a browser server endpoint directly
57
- prima browser start a prima-owned browser instead, when no session is open
58
- --instance <name> which prima-owned browser you talk to; parallel work
59
- needs one each
60
- --session [file] cookies and storage persisted across processes; ignored
61
- while attached, the attached session keeps its own
62
- Prima never launches a browser implicitly and never closes an attached one - it
63
- disconnects. browser list shows both kinds; ### Instance names the one you are on.
64
- Every browser is reached over a Playwright browser-server endpoint, which needs the
65
- Node build - run prima as "npx explorbot prima ..." or through the published prima
66
- bin; from source under Bun the connection does not open.
67
- When no AI model is usable pw still works; for everything else drive
68
- playwright-cli directly.
69
- Parsed but not active yet: --framework, so reported code is CodeceptJS whatever
70
- you pass.
18
+ prima check "a workflow can be created and appears in the list" --expected "the new workflow is listed"
19
+ prima do "open the account menu" "choose the settings entry" "switch the theme to dark" "check it took effect"
20
+ prima pw "({ page }) => page.click('[data-test=submit]')"
21
+ `;
22
+ const checkHelp = dedent `
23
+ check takes an outcome rather than a click path, and works out how to reach it.
24
+ --expected one outcome the run must reach, repeatable for several. Without it the
25
+ scenario text is the single expected outcome. Each comes back under
26
+ ### Expected outcomes as PASSED, FAILED or not verified - "not verified"
27
+ means the run never checked it, which is not the same as false.
28
+ Page problems seen on the way appear under ### Answer, not as step failures.
29
+ `;
30
+ const doHelp = dedent `
31
+ Each instruction is numbered and accounted for: ### Steps reports each as ok or FAIL
32
+ with what proved it. One that could not be carried out fails the command and says why.
33
+ Nothing runs past the last instruction given. A whole remaining sequence in one call is
34
+ what makes this tier cheap.
35
+ `;
36
+ const verifyHelp = dedent `
37
+ Reports each assertion it could express as PASSED or FAILED with its playwright form,
38
+ and gives no overall verdict - read the lines and decide. "none ran" means the claim
39
+ could not be expressed, which is not the same as false.
40
+ `;
41
+ const reportHelp = dedent `
42
+ Commands are logged as they run, so the report needs no browser and outlives the session.
43
+ The most recent session is reported unless --pw-session names another.
44
+ `;
45
+ const sessionHelp = dedent `
46
+ --endpoint <ep> attach to a browser server endpoint directly, skipping discovery
47
+ --instance <name> which prima-owned browser you talk to; parallel work needs one each
48
+ --session [file] cookies and storage persisted across processes; ignored while
49
+ attached, since the attached session keeps its own
50
+ --framework parsed but not active yet; reported code is CodeceptJS either way
51
+ DEBUG='explorbot:*' in front of a command prints the log of everything it does.
52
+ When no AI model is usable pw still works; for everything else drive playwright-cli.
71
53
  `;
72
- function buildOptions(options) {
54
+ let rootOptions = () => ({});
55
+ function buildOptions(subcommand) {
56
+ const options = { ...rootOptions(), ...stripEmpty(subcommand) };
73
57
  return {
74
- verbose: options.verbose || options.debug,
75
58
  config: options.config,
76
59
  path: options.path,
77
60
  instance: options.instance,
78
61
  session: options.session,
79
- heal: options.heal,
80
62
  ephemeral: options.ephemeral,
81
63
  framework: options.framework,
82
64
  noVision: options.vision === false,
@@ -88,29 +70,38 @@ function buildOptions(options) {
88
70
  pwSession: options.pwSession,
89
71
  };
90
72
  }
73
+ function stripEmpty(options) {
74
+ const present = {};
75
+ for (const [key, value] of Object.entries(options || {})) {
76
+ if (value === undefined)
77
+ continue;
78
+ present[key] = value;
79
+ }
80
+ return present;
81
+ }
91
82
  function addCommonOptions(cmd) {
92
83
  return cmd
93
- .option('-v, --verbose', 'Enable verbose logging')
94
- .option('--debug', 'Enable debug logging (same as --verbose)')
95
84
  .option('-c, --config <path>', 'Path to explorbot configuration file')
96
85
  .option('-p, --path <path>', 'Working directory path')
97
86
  .option('-i, --instance <name>', 'Browser instance to drive')
98
87
  .option('--session [file]', 'Persist cookies and storage to a session file')
99
- .option('--no-heal', 'Fail immediately instead of letting AI retry a failed action')
100
88
  .option('--ephemeral', 'Keep no state between runs; applies to config-free runs, where output goes to a temp directory')
101
89
  .option('--framework <name>', 'Not active yet: framework the reported code targets, codeceptjs or playwright')
102
90
  .option('--url <url>', 'Page to open when the session has no page yet')
103
91
  .option('--endpoint <ep>', 'Websocket endpoint of a browser server to attach to, skipping discovery')
104
- .option('--pw-session <title>', 'Title of the playwright-cli session to attach to');
92
+ .option('--pw-session <title>', 'Title of the playwright-cli session to attach to')
93
+ .addHelpText('after', `\n${sessionHelp}`);
105
94
  }
106
95
  function primaFor(options) {
107
- setPreserveConsoleLogs(true);
108
96
  if (options.ephemeral)
109
97
  process.env.EXPLORBOT_EPHEMERAL = '1';
110
98
  return new Prima(buildOptions(options));
111
99
  }
112
- async function runPrima(options, command, run) {
100
+ async function runPrima(options, command, run, record = true) {
101
+ setQuietMode(!isVerboseMode());
102
+ trackActivityLine();
113
103
  const prima = primaFor(options);
104
+ const startedAt = Date.now();
114
105
  let envelope;
115
106
  try {
116
107
  await prima.start();
@@ -119,6 +110,9 @@ async function runPrima(options, command, run) {
119
110
  catch (error) {
120
111
  envelope = await prima.toolFailureEnvelope(command, error);
121
112
  }
113
+ if (record)
114
+ prima.record(envelope, Date.now() - startedAt);
115
+ clearActivityLine();
122
116
  console.log(renderEnvelope(envelope));
123
117
  await prima.stop().catch(() => { });
124
118
  process.exit(envelope.ok ? 0 : 1);
@@ -136,24 +130,31 @@ async function runBrowser(options, run) {
136
130
  }
137
131
  export function createPrimaCommands(name = 'prima') {
138
132
  const cmd = new Command(name);
139
- cmd.description('Drive an already-open browser one command at a time and report back in a plain-text envelope');
133
+ cmd.description('Tests and drives a web app through described behaviour instead of locators: one command carries a whole scenario, verifies it, and reports the proof');
134
+ cmd.option('--pw-session <title>', 'Title of the playwright-cli session to attach to');
135
+ cmd.option('--url <url>', 'Page to open when the session has no page yet');
140
136
  cmd.addHelpText('after', `\n${helpContract}`);
137
+ rootOptions = () => cmd.opts();
141
138
  addCommonOptions(cmd.command('pw <fn>').description('Run a Playwright function expression against the open page')).action(async (fn, options) => {
142
139
  await runPrima(options, `pw ${fn}`, (prima) => prima.pw(fn));
143
140
  });
144
- addCommonOptions(cmd.command('do <instructions...>').description('Run high-level instructions tester-style, one argument per instruction')).action(async (instructions, options) => {
141
+ addCommonOptions(cmd.command('do <instructions...>').description('Run high-level instructions tester-style, one argument per instruction'))
142
+ .addHelpText('after', `\n${doHelp}`)
143
+ .action(async (instructions, options) => {
145
144
  await runPrima(options, `do ${instructions.join(' ')}`, (prima) => prima.do(instructions));
146
145
  });
147
- addCommonOptions(cmd.command('click <target>').description('Click an element described in plain words')).action(async (target, options) => {
148
- await runPrima(options, `click ${target}`, (prima) => prima.click(target));
149
- });
150
- addCommonOptions(cmd.command('fill <field> <value>').description('Fill a field described in plain words')).action(async (field, value, options) => {
151
- await runPrima(options, `fill ${field} ${value}`, (prima) => prima.fill(field, value));
146
+ addCommonOptions(cmd.command('check <scenario>').description('Run a scenario end to end as a test, with its own verification, and report the steps it took'))
147
+ .option('--expected <outcome>', 'An outcome the run must reach; repeat the flag for several', (value, all) => [...all, value], [])
148
+ .addHelpText('after', `\n${checkHelp}`)
149
+ .action(async (scenario, options) => {
150
+ await runPrima(options, `check ${scenario}`, (prima) => prima.check(scenario, options.expected));
152
151
  });
153
152
  addCommonOptions(cmd.command('ask <question>').description('Answer a question about the current page').option('--no-vision', 'Answer from page structure only, without a screenshot')).action(async (question, options) => {
154
153
  await runPrima(options, `ask ${question}`, (prima) => prima.ask(question));
155
154
  });
156
- addCommonOptions(cmd.command('verify <assertion>').alias('assert').description('Assert a statement about the current page')).action(async (assertion, options) => {
155
+ addCommonOptions(cmd.command('verify <assertion>').alias('assert').description('Assert a statement about the current page'))
156
+ .addHelpText('after', `\n${verifyHelp}`)
157
+ .action(async (assertion, options) => {
157
158
  await runPrima(options, `verify ${assertion}`, (prima) => prima.verify(assertion));
158
159
  });
159
160
  addCommonOptions(cmd.command('research').description('Map the current page and return verified locators').option('--data', 'Include data extraction in the map').option('--deep', 'Expand hidden elements for a deeper map').option('--fresh', 'Ignore the cached map and research the page again')).action(async (options) => {
@@ -164,6 +165,25 @@ export function createPrimaCommands(name = 'prima') {
164
165
  options.baseUrl = target;
165
166
  await runPrima(options, `go ${target}`, (prima) => prima.go(target));
166
167
  });
168
+ addCommonOptions(cmd.command('config').description('Show the AI models prima runs on and the config file they come from')).action(async (options) => {
169
+ setQuietMode(!isVerboseMode());
170
+ const prima = primaFor(options);
171
+ console.log(await prima.config().catch((error) => browserErrorMessage(error)));
172
+ await prima.stop().catch(() => { });
173
+ process.exit(0);
174
+ });
175
+ addCommonOptions(cmd.command('status <hash>').description('Show the artifacts and page detail recorded for an earlier command')).action(async (hash, options) => {
176
+ await runPrima(options, `status ${hash}`, (prima) => prima.status(hash), false);
177
+ });
178
+ addCommonOptions(cmd.command('report').description('Turn every command of a session into one html and markdown report'))
179
+ .addHelpText('after', `\n${reportHelp}`)
180
+ .action(async (options) => {
181
+ setQuietMode(!isVerboseMode());
182
+ console.log(await primaFor(options)
183
+ .report()
184
+ .catch((error) => browserErrorMessage(error)));
185
+ process.exit(0);
186
+ });
167
187
  const browser = cmd.command('browser').description('Manage the browsers prima drives');
168
188
  addCommonOptions(browser.command('start').description('Start a prima-owned browser and hold it open until Ctrl+C'))
169
189
  .option('-s, --show', 'Launch the browser in a visible window')
@@ -1,7 +1,12 @@
1
1
  import { mkdirSync, writeFileSync } from 'node:fs';
2
2
  import path from 'node:path';
3
+ const EXPECTATION_LABELS = {
4
+ passed: 'PASSED ',
5
+ failed: 'FAILED ',
6
+ unverified: 'not verified',
7
+ };
3
8
  export function renderEnvelope(data) {
4
- const sections = [renderResult(data), renderPage(data), renderOutcome(data), ...renderFailure(data), renderInstance(data.instance), renderArtifacts(data)];
9
+ const sections = [renderResult(data), renderPage(data), renderValue(data), renderChanges(data), renderSteps(data), renderExpectations(data), renderOutcome(data), ...renderFailure(data), renderInstance(data), renderArtifacts(data)];
5
10
  return sections.filter((section) => section).join('\n\n');
6
11
  }
7
12
  export function writeArtifacts(dir, snapshot) {
@@ -9,29 +14,21 @@ export function writeArtifacts(dir, snapshot) {
9
14
  const paths = {
10
15
  aria: path.resolve(dir, 'aria.yml'),
11
16
  html: path.resolve(dir, 'page.html'),
12
- network: path.resolve(dir, 'network.jsonl'),
13
17
  };
14
18
  writeFileSync(paths.aria, snapshot.aria ?? '', 'utf-8');
15
19
  writeFileSync(paths.html, snapshot.html ?? '', 'utf-8');
20
+ if (!snapshot.requests.length)
21
+ return paths;
22
+ paths.network = path.resolve(dir, 'network.jsonl');
16
23
  writeFileSync(paths.network, snapshot.requests.map((request) => `${JSON.stringify(request)}\n`).join(''), 'utf-8');
17
24
  return paths;
18
25
  }
19
26
  function renderResult(data) {
20
27
  const lines = [`ok: ${data.ok}`, `command: ${data.command}`];
21
- const healed = renderHealed(data);
22
- if (healed)
23
- lines.push(healed);
24
28
  if (data.used?.length)
25
29
  lines.push(`used: ${data.used.join('; ')}`);
26
30
  return section('Result', lines.join('\n'));
27
31
  }
28
- function renderHealed(data) {
29
- if (data.healed === undefined)
30
- return null;
31
- if (data.healNote)
32
- return `healed: ${data.healed} (${data.healNote})`;
33
- return `healed: ${data.healed}`;
34
- }
35
32
  function renderPage(data) {
36
33
  const { url, previousUrl, title, state, visits } = data.page;
37
34
  const urlLabel = `url: ${url}`;
@@ -43,67 +40,96 @@ function renderPage(data) {
43
40
  const lines = [align(urlLabel, changedMarker, width), `title: ${title}`, align(stateLabel, `(visit #${visits})`, width)];
44
41
  return section('Page', lines.join('\n'));
45
42
  }
43
+ function renderValue(data) {
44
+ if (data.value === undefined)
45
+ return null;
46
+ return section('Value', data.value);
47
+ }
48
+ function renderChanges(data) {
49
+ if (data.changes === undefined || data.changes === null)
50
+ return null;
51
+ return section('Changes', data.changes);
52
+ }
53
+ function renderSteps(data) {
54
+ if (!data.steps?.length)
55
+ return null;
56
+ const lines = [];
57
+ data.steps.forEach((step, index) => {
58
+ lines.push(`${index + 1}. ${step.ok ? 'ok ' : 'FAIL'} ${step.label}`);
59
+ for (const line of (step.proof || '').split('\n').filter(Boolean))
60
+ lines.push(` ${line}`);
61
+ });
62
+ if (data.stepFiles)
63
+ lines.push('', `page after each step: ${data.stepFiles}`);
64
+ return section('Steps', lines.join('\n'));
65
+ }
66
+ function renderExpectations(data) {
67
+ if (!data.expectations?.length)
68
+ return null;
69
+ const lines = data.expectations.map((expectation, index) => `${index + 1}. ${EXPECTATION_LABELS[expectation.status]} ${expectation.text}`);
70
+ return section('Expected outcomes', lines.join('\n'));
71
+ }
46
72
  function renderOutcome(data) {
47
- if (data.changes)
48
- return section('Changes', data.changes);
49
73
  if (data.answer)
50
74
  return section('Answer', data.answer);
51
75
  if (data.research)
52
76
  return section('Research', data.research);
53
- if (!data.verdict)
77
+ if (!data.assertions)
54
78
  return null;
55
- const lines = [`passed: ${data.verdict.passed}`, `evidence: ${data.verdict.evidence}`, `code: ${data.verdict.code}`];
56
- return section('Verdict', lines.join('\n'));
79
+ if (!data.assertions.length)
80
+ return section('Assertions', 'none ran — no assertion could express this claim, so nothing was checked against the page');
81
+ const lines = data.assertions.map((assertion) => {
82
+ const code = assertion.code
83
+ .split('\n')
84
+ .map((line) => line.trim())
85
+ .filter((line) => line && !line.startsWith('//'))
86
+ .join(' ');
87
+ return `${code} => ${assertion.passed ? 'PASSED' : 'FAILED'}`;
88
+ });
89
+ const proof = data.assertions.flatMap((assertion) => assertion.proof);
90
+ if (proof.length)
91
+ lines.push('', 'playwright:', ...proof);
92
+ return section('Assertions', lines.join('\n'));
57
93
  }
58
94
  function renderFailure(data) {
59
95
  if (!data.failure)
60
96
  return [];
61
- const lines = [`error: ${data.failure.error}`];
62
- if (data.failure.reasoning)
63
- lines.push(`reasoning: ${data.failure.reasoning}`);
64
- return [section('Failure', lines.join('\n')), renderAttempts(data.failure.attempts), renderCompactAria(data.failure.compactAria)];
65
- }
66
- function renderAttempts(attempts) {
67
- if (!attempts?.length)
68
- return null;
69
- const labels = attempts.map((attempt, index) => `${index + 1}. ${attempt.code}`);
70
- const width = Math.max(...labels.map((label) => label.length)) + 3;
71
- const lines = labels.map((label, index) => align(label, `→ ${attempts[index].outcome}`, width));
72
- return section(`Healing attempts (${attempts.length})`, lines.join('\n'));
97
+ return [section('Failure', `error: ${data.failure.error}`), renderCompactAria(data.failure.compactAria)];
73
98
  }
74
99
  function renderCompactAria(compactAria) {
75
100
  if (!compactAria)
76
101
  return null;
77
102
  return section('Current page (compact ARIA)', compactAria);
78
103
  }
79
- function renderInstance(instance) {
80
- const others = instance.others.map((other) => `${other.name} (${tabsLabel(other.tabs)})`);
81
- const lines = [`instance: ${instance.name} (${tabsLabel(instance.tabs)}) | other instances: ${otherInstances(others)}`, browserLine(instance)];
82
- return section('Instance', lines.join('\n'));
83
- }
84
- function otherInstances(others) {
85
- if (!others.length)
86
- return 'none';
87
- return others.join(', ');
104
+ function renderInstance(data) {
105
+ const instance = data.instance;
106
+ const parts = [`${instance.name} (${tabsLabel(instance.tabs)})`, browserLine(instance)];
107
+ if (instance.others.length)
108
+ parts.push(`other instances: ${instance.others.map((other) => `${other.name} (${tabsLabel(other.tabs)})`).join(', ')}`);
109
+ if (data.status)
110
+ parts.push(`details: prima status ${data.status}`);
111
+ return section('Instance', parts.join(' | '));
88
112
  }
89
113
  function browserLine(instance) {
90
114
  if (instance.attached)
91
- return `browser: attached (${instance.attached})`;
115
+ return `attached to ${instance.attached}`;
92
116
  if (instance.startedAgo)
93
- return `browser: running, started ${instance.startedAgo} ago`;
117
+ return `running, started ${instance.startedAgo} ago`;
94
118
  if (instance.tabs > 0)
95
- return 'browser: running';
96
- return 'browser: not running';
119
+ return 'running';
120
+ return 'not running';
97
121
  }
98
122
  function tabsLabel(tabs) {
99
123
  if (tabs === 1)
100
124
  return '1 tab';
101
125
  return `${tabs} tabs`;
102
126
  }
103
- function renderArtifacts(data) {
127
+ export function renderArtifacts(data) {
104
128
  if (!data.artifacts)
105
129
  return null;
106
- const lines = [`aria: ${data.artifacts.aria}`, `html: ${data.artifacts.html}`, `network: ${data.artifacts.network}`];
130
+ const lines = [`aria: ${data.artifacts.aria}`, `html: ${data.artifacts.html}`];
131
+ if (data.artifacts.network)
132
+ lines.push(`network: ${data.artifacts.network}`);
107
133
  return section('Artifacts', lines.join('\n'));
108
134
  }
109
135
  function align(label, marker, width) {