explorbot 0.2.3 → 0.2.5

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 (174) hide show
  1. package/README.md +1 -1
  2. package/bin/explorbot-cli.ts +26 -8
  3. package/boat/api-tester/src/cli.ts +17 -0
  4. package/boat/api-tester/src/config.ts +4 -2
  5. package/boat/doc-collector/bin/doc-collector-cli.ts +2 -0
  6. package/boat/doc-collector/src/ai/documentarian.ts +61 -31
  7. package/boat/doc-collector/src/cli.ts +14 -1
  8. package/boat/doc-collector/src/config.ts +4 -2
  9. package/boat/prima/bin/prima-cli.ts +0 -0
  10. package/boat/prima/src/activity-line.ts +33 -0
  11. package/boat/prima/src/cli.ts +127 -86
  12. package/boat/prima/src/envelope.ts +102 -52
  13. package/boat/prima/src/prima.ts +567 -128
  14. package/boat/prima/src/pw-parser.ts +11 -1
  15. package/boat/prima/src/pw-registry.ts +4 -5
  16. package/boat/prima/src/session-log.ts +126 -0
  17. package/dist/bin/explorbot-cli.js +26 -8
  18. package/dist/boat/api-tester/bin/apibot-cli.js +2 -0
  19. package/dist/boat/api-tester/src/cli.js +17 -0
  20. package/dist/boat/api-tester/src/config.js +4 -2
  21. package/dist/boat/doc-collector/bin/doc-collector-cli.js +2 -0
  22. package/dist/boat/doc-collector/src/ai/documentarian.js +44 -19
  23. package/dist/boat/doc-collector/src/cli.js +14 -1
  24. package/dist/boat/doc-collector/src/config.js +4 -2
  25. package/dist/boat/prima/src/activity-line.js +30 -0
  26. package/dist/boat/prima/src/cli.js +109 -77
  27. package/dist/boat/prima/src/envelope.js +94 -44
  28. package/dist/boat/prima/src/prima.js +533 -119
  29. package/dist/boat/prima/src/pw-parser.js +13 -1
  30. package/dist/boat/prima/src/pw-registry.js +4 -5
  31. package/dist/boat/prima/src/session-log.js +108 -0
  32. package/dist/package.json +3 -2
  33. package/dist/rules/navigator/verification-actions.md +20 -0
  34. package/dist/src/action-result.d.ts +7 -0
  35. package/dist/src/action-result.js +4 -0
  36. package/dist/src/action.d.ts +2 -0
  37. package/dist/src/action.js +41 -2
  38. package/dist/src/ai/captain/web-mode.js +6 -3
  39. package/dist/src/ai/captain.js +2 -0
  40. package/dist/src/ai/navigator.d.ts +34 -0
  41. package/dist/src/ai/navigator.js +237 -181
  42. package/dist/src/ai/pilot.d.ts +7 -0
  43. package/dist/src/ai/pilot.js +90 -2
  44. package/dist/src/ai/provider.d.ts +2 -2
  45. package/dist/src/ai/provider.js +14 -23
  46. package/dist/src/ai/rerunner.js +2 -1
  47. package/dist/src/ai/researcher/cache.d.ts +2 -0
  48. package/dist/src/ai/researcher/cache.js +10 -2
  49. package/dist/src/ai/researcher.js +3 -2
  50. package/dist/src/ai/rules.js +17 -10
  51. package/dist/src/ai/session-analyst.js +2 -0
  52. package/dist/src/ai/task-agent.js +4 -1
  53. package/dist/src/ai/tester.d.ts +6 -3
  54. package/dist/src/ai/tester.js +50 -46
  55. package/dist/src/ai/tools.d.ts +14 -0
  56. package/dist/src/ai/tools.js +117 -37
  57. package/dist/src/commands/config-command.d.ts +51 -0
  58. package/dist/src/commands/config-command.js +117 -0
  59. package/dist/src/commands/index.js +2 -0
  60. package/dist/src/config.d.ts +9 -1
  61. package/dist/src/config.js +53 -4
  62. package/dist/src/execution-controller.d.ts +2 -0
  63. package/dist/src/execution-controller.js +6 -0
  64. package/dist/src/explorbot.d.ts +2 -1
  65. package/dist/src/explorbot.js +7 -2
  66. package/dist/src/explorer.js +2 -3
  67. package/dist/src/playwright-recorder.js +30 -0
  68. package/dist/src/remote.d.ts +55 -0
  69. package/dist/src/remote.js +235 -0
  70. package/dist/src/reporter.d.ts +1 -0
  71. package/dist/src/reporter.js +7 -1
  72. package/dist/src/state-manager.d.ts +2 -1
  73. package/dist/src/state-manager.js +3 -1
  74. package/dist/src/stats.d.ts +1 -0
  75. package/dist/src/stats.js +1 -0
  76. package/dist/src/test-plan.d.ts +3 -0
  77. package/dist/src/test-plan.js +26 -0
  78. package/dist/src/utils/aria.d.ts +2 -8
  79. package/dist/src/utils/aria.js +69 -40
  80. package/dist/src/utils/html.js +1 -0
  81. package/dist/src/utils/logger.d.ts +7 -1
  82. package/dist/src/utils/logger.js +32 -0
  83. package/dist/src/utils/page-readiness.js +18 -1
  84. package/dist/src/utils/url-matcher.js +3 -0
  85. package/dist/src/utils/web-element.d.ts +2 -0
  86. package/dist/src/utils/web-element.js +8 -0
  87. package/dist/src/utils/web-sandbox.d.ts +1 -1
  88. package/dist/src/utils/web-sandbox.js +2 -3
  89. package/docs/api-testing/basics.md +90 -0
  90. package/docs/api-testing/planning.md +57 -0
  91. package/docs/api-testing/running-tests.md +55 -0
  92. package/docs/assets/cloud-report.png +0 -0
  93. package/docs/assets/html-report.png +0 -0
  94. package/docs/assets/langfuse-trace.png +0 -0
  95. package/docs/assets/successful-explore-run.png +0 -0
  96. package/docs/basics/getting-started.md +140 -0
  97. package/docs/basics/prerequisites.md +63 -0
  98. package/docs/basics/providers.md +362 -0
  99. package/docs/basics/running.md +78 -0
  100. package/docs/contributing/ai-integration-tests.md +57 -0
  101. package/docs/contributing/contributing.md +90 -0
  102. package/docs/contributing/demo-videos.md +36 -0
  103. package/docs/contributing/npm-package.md +138 -0
  104. package/docs/contributing/observability.md +227 -0
  105. package/docs/contributing/regression-tests.md +103 -0
  106. package/docs/contributing/testing.md +95 -0
  107. package/docs/doc-collection/basics.md +128 -0
  108. package/docs/doc-collection/crawling.md +67 -0
  109. package/docs/doc-collection/interactive-mode.md +99 -0
  110. package/docs/index.json +87 -0
  111. package/docs/reference/commands.md +997 -0
  112. package/docs/reference/configuration.md +569 -0
  113. package/docs/reference/scripting.md +303 -0
  114. package/docs/reference/websocket.md +50 -0
  115. package/docs/superpowers/plans/2026-08-01-actor-boat.md +925 -0
  116. package/docs/superpowers/plans/2026-08-01-prima-boat.md +1120 -0
  117. package/docs/superpowers/reviews/2026-08-06-prima-vs-playwright-cli.md +268 -0
  118. package/docs/superpowers/specs/2026-08-01-actor-boat-design.md +204 -0
  119. package/docs/superpowers/specs/2026-08-01-prima-boat-design.md +242 -0
  120. package/docs/superpowers/specs/2026-08-03-global-config-design.md +138 -0
  121. package/docs/superpowers/specs/2026-08-07-prima-fixes-design.md +394 -0
  122. package/docs/superpowers/specs/2026-08-18-prima-false-verdicts.md +159 -0
  123. package/docs/web-testing/agents.md +158 -0
  124. package/docs/web-testing/automated-tests.md +134 -0
  125. package/docs/web-testing/basics.md +91 -0
  126. package/docs/web-testing/customization.md +131 -0
  127. package/docs/web-testing/hooks.md +238 -0
  128. package/docs/web-testing/page-interaction.md +84 -0
  129. package/docs/web-testing/planner.md +122 -0
  130. package/docs/web-testing/rerun.md +164 -0
  131. package/docs/web-testing/researcher.md +380 -0
  132. package/docs/workflow/agentic-usage.md +233 -0
  133. package/docs/workflow/application-spec.md +73 -0
  134. package/docs/workflow/ci.md +202 -0
  135. package/docs/workflow/knowledge.md +310 -0
  136. package/docs/workflow/planning-styles.md +67 -0
  137. package/docs/workflow/reporting.md +133 -0
  138. package/docs/workflow/test-plans.md +90 -0
  139. package/package.json +3 -2
  140. package/rules/navigator/verification-actions.md +20 -0
  141. package/src/action-result.ts +11 -0
  142. package/src/action.ts +43 -3
  143. package/src/ai/captain/web-mode.ts +6 -3
  144. package/src/ai/captain.ts +3 -0
  145. package/src/ai/navigator.ts +255 -186
  146. package/src/ai/pilot.ts +104 -2
  147. package/src/ai/provider.ts +14 -24
  148. package/src/ai/rerunner.ts +2 -1
  149. package/src/ai/researcher/cache.ts +12 -2
  150. package/src/ai/researcher.ts +3 -2
  151. package/src/ai/rules.ts +17 -10
  152. package/src/ai/session-analyst.ts +2 -0
  153. package/src/ai/task-agent.ts +3 -1
  154. package/src/ai/tester.ts +52 -45
  155. package/src/ai/tools.ts +136 -37
  156. package/src/commands/config-command.ts +146 -0
  157. package/src/commands/index.ts +2 -0
  158. package/src/config.ts +60 -5
  159. package/src/execution-controller.ts +8 -0
  160. package/src/explorbot.ts +7 -3
  161. package/src/explorer.ts +2 -2
  162. package/src/playwright-recorder.ts +23 -0
  163. package/src/remote.ts +244 -0
  164. package/src/reporter.ts +7 -1
  165. package/src/state-manager.ts +6 -2
  166. package/src/stats.ts +1 -0
  167. package/src/test-plan.ts +29 -0
  168. package/src/utils/aria.ts +65 -45
  169. package/src/utils/html.ts +1 -0
  170. package/src/utils/logger.ts +33 -2
  171. package/src/utils/page-readiness.ts +24 -1
  172. package/src/utils/url-matcher.ts +3 -0
  173. package/src/utils/web-element.ts +9 -0
  174. package/src/utils/web-sandbox.ts +3 -4
@@ -2,81 +2,73 @@ 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. It runs
24
+ on the page you are already on and never reloads it, so an open dialog survives the check.
25
+ --expected one outcome the run must reach, repeatable for several. Without it the
26
+ scenario text is the single expected outcome. Each comes back under
27
+ ### Expected outcomes as PASSED, FAILED, CONTRADICTION or not verified.
28
+ "not verified" means the run never checked it, which is not the same
29
+ as false.
30
+ Outcomes are settled against a screenshot of the whole page: what a user can see is
31
+ the proof, and the run log only says what was done. CONTRADICTION means the two
32
+ disagree - reported with both sides rather than settled one way, and ### Artifacts
33
+ then names the html, aria and screenshot on disk so you can judge it yourself. Not
34
+ finding something in the picture is not enough on its own; that is "not verified".
35
+ ok: follows those outcomes - false when one FAILED or CONTRADICTED, or when the run
36
+ could not complete, which is reported as such rather than as an app failure.
37
+ Page problems seen on the way appear under ### Answer, not as step failures.
38
+ `;
39
+ const doHelp = dedent `
40
+ Each instruction is numbered and accounted for: ### Steps reports each as ok, FAIL or ??.
41
+ ?? means the action ran but the run ended without confirming that instruction - read the
42
+ steps above it. Only FAIL and an instruction the page could not carry out fail the command.
43
+ Nothing runs past the last instruction given. A whole remaining sequence in one call is
44
+ what makes this tier cheap.
71
45
  `;
72
- function buildOptions(options) {
46
+ const verifyHelp = dedent `
47
+ Reports each assertion it could express as PASSED or FAILED with its playwright form,
48
+ and gives no overall verdict - read the lines and decide. "none ran" means the claim
49
+ could not be expressed, which is not the same as false.
50
+ `;
51
+ const reportHelp = dedent `
52
+ Commands are logged as they run, so the report needs no browser and outlives the session.
53
+ The most recent session is reported unless --pw-session names another.
54
+ `;
55
+ const sessionHelp = dedent `
56
+ --endpoint <ep> attach to a browser server endpoint directly, skipping discovery
57
+ --instance <name> which prima-owned browser you talk to; parallel work needs one each
58
+ --session [file] cookies and storage persisted across processes; ignored while
59
+ attached, since the attached session keeps its own
60
+ --framework parsed but not active yet; reported code is CodeceptJS either way
61
+ DEBUG='explorbot:*' in front of a command prints the log of everything it does.
62
+ When no AI model is usable pw still works; for everything else drive playwright-cli.
63
+ `;
64
+ let rootOptions = () => ({});
65
+ function buildOptions(subcommand) {
66
+ const options = { ...rootOptions(), ...stripEmpty(subcommand) };
73
67
  return {
74
- verbose: options.verbose || options.debug,
75
68
  config: options.config,
76
69
  path: options.path,
77
70
  instance: options.instance,
78
71
  session: options.session,
79
- heal: options.heal,
80
72
  ephemeral: options.ephemeral,
81
73
  framework: options.framework,
82
74
  noVision: options.vision === false,
@@ -88,29 +80,38 @@ function buildOptions(options) {
88
80
  pwSession: options.pwSession,
89
81
  };
90
82
  }
83
+ function stripEmpty(options) {
84
+ const present = {};
85
+ for (const [key, value] of Object.entries(options || {})) {
86
+ if (value === undefined)
87
+ continue;
88
+ present[key] = value;
89
+ }
90
+ return present;
91
+ }
91
92
  function addCommonOptions(cmd) {
92
93
  return cmd
93
- .option('-v, --verbose', 'Enable verbose logging')
94
- .option('--debug', 'Enable debug logging (same as --verbose)')
95
94
  .option('-c, --config <path>', 'Path to explorbot configuration file')
96
95
  .option('-p, --path <path>', 'Working directory path')
97
96
  .option('-i, --instance <name>', 'Browser instance to drive')
98
97
  .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
98
  .option('--ephemeral', 'Keep no state between runs; applies to config-free runs, where output goes to a temp directory')
101
99
  .option('--framework <name>', 'Not active yet: framework the reported code targets, codeceptjs or playwright')
102
100
  .option('--url <url>', 'Page to open when the session has no page yet')
103
101
  .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');
102
+ .option('--pw-session <title>', 'Title of the playwright-cli session to attach to')
103
+ .addHelpText('after', `\n${sessionHelp}`);
105
104
  }
106
105
  function primaFor(options) {
107
- setPreserveConsoleLogs(true);
108
106
  if (options.ephemeral)
109
107
  process.env.EXPLORBOT_EPHEMERAL = '1';
110
108
  return new Prima(buildOptions(options));
111
109
  }
112
- async function runPrima(options, command, run) {
110
+ async function runPrima(options, command, run, record = true) {
111
+ setQuietMode(!isVerboseMode());
112
+ trackActivityLine();
113
113
  const prima = primaFor(options);
114
+ const startedAt = Date.now();
114
115
  let envelope;
115
116
  try {
116
117
  await prima.start();
@@ -119,6 +120,9 @@ async function runPrima(options, command, run) {
119
120
  catch (error) {
120
121
  envelope = await prima.toolFailureEnvelope(command, error);
121
122
  }
123
+ if (record)
124
+ prima.record(envelope, Date.now() - startedAt);
125
+ clearActivityLine();
122
126
  console.log(renderEnvelope(envelope));
123
127
  await prima.stop().catch(() => { });
124
128
  process.exit(envelope.ok ? 0 : 1);
@@ -136,24 +140,31 @@ async function runBrowser(options, run) {
136
140
  }
137
141
  export function createPrimaCommands(name = 'prima') {
138
142
  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');
143
+ 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');
144
+ cmd.option('--pw-session <title>', 'Title of the playwright-cli session to attach to');
145
+ cmd.option('--url <url>', 'Page to open when the session has no page yet');
140
146
  cmd.addHelpText('after', `\n${helpContract}`);
147
+ rootOptions = () => cmd.opts();
141
148
  addCommonOptions(cmd.command('pw <fn>').description('Run a Playwright function expression against the open page')).action(async (fn, options) => {
142
149
  await runPrima(options, `pw ${fn}`, (prima) => prima.pw(fn));
143
150
  });
144
- addCommonOptions(cmd.command('do <instructions...>').description('Run high-level instructions tester-style, one argument per instruction')).action(async (instructions, options) => {
151
+ addCommonOptions(cmd.command('do <instructions...>').description('Run high-level instructions tester-style, one argument per instruction'))
152
+ .addHelpText('after', `\n${doHelp}`)
153
+ .action(async (instructions, options) => {
145
154
  await runPrima(options, `do ${instructions.join(' ')}`, (prima) => prima.do(instructions));
146
155
  });
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));
156
+ 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'))
157
+ .option('--expected <outcome>', 'An outcome the run must reach; repeat the flag for several', (value, all) => [...all, value], [])
158
+ .addHelpText('after', `\n${checkHelp}`)
159
+ .action(async (scenario, options) => {
160
+ await runPrima(options, `check ${scenario}`, (prima) => prima.check(scenario, options.expected));
152
161
  });
153
162
  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
163
  await runPrima(options, `ask ${question}`, (prima) => prima.ask(question));
155
164
  });
156
- addCommonOptions(cmd.command('verify <assertion>').alias('assert').description('Assert a statement about the current page')).action(async (assertion, options) => {
165
+ addCommonOptions(cmd.command('verify <assertion>').alias('assert').description('Assert a statement about the current page'))
166
+ .addHelpText('after', `\n${verifyHelp}`)
167
+ .action(async (assertion, options) => {
157
168
  await runPrima(options, `verify ${assertion}`, (prima) => prima.verify(assertion));
158
169
  });
159
170
  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 +175,27 @@ export function createPrimaCommands(name = 'prima') {
164
175
  options.baseUrl = target;
165
176
  await runPrima(options, `go ${target}`, (prima) => prima.go(target));
166
177
  });
178
+ addCommonOptions(cmd.command('config').description('Show models, config file and paths used by this run'))
179
+ .option('--json', 'Print the resolved config as JSON')
180
+ .action(async (options) => {
181
+ setQuietMode(!isVerboseMode());
182
+ const prima = primaFor(options);
183
+ console.log(await prima.config(options.json).catch((error) => browserErrorMessage(error)));
184
+ await prima.stop().catch(() => { });
185
+ process.exit(0);
186
+ });
187
+ addCommonOptions(cmd.command('status <hash>').description('Show the artifacts and page detail recorded for an earlier command')).action(async (hash, options) => {
188
+ await runPrima(options, `status ${hash}`, (prima) => prima.status(hash), false);
189
+ });
190
+ addCommonOptions(cmd.command('report').description('Turn every command of a session into one html and markdown report'))
191
+ .addHelpText('after', `\n${reportHelp}`)
192
+ .action(async (options) => {
193
+ setQuietMode(!isVerboseMode());
194
+ console.log(await primaFor(options)
195
+ .report()
196
+ .catch((error) => browserErrorMessage(error)));
197
+ process.exit(0);
198
+ });
167
199
  const browser = cmd.command('browser').description('Manage the browsers prima drives');
168
200
  addCommonOptions(browser.command('start').description('Start a prima-owned browser and hold it open until Ctrl+C'))
169
201
  .option('-s, --show', 'Launch the browser in a visible window')
@@ -1,7 +1,13 @@
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
+ contradiction: 'CONTRADICTION',
8
+ };
3
9
  export function renderEnvelope(data) {
4
- const sections = [renderResult(data), renderPage(data), renderOutcome(data), ...renderFailure(data), renderInstance(data.instance), renderArtifacts(data)];
10
+ const sections = [renderResult(data), renderPage(data), renderValue(data), renderChanges(data), renderSteps(data), renderExpectations(data), renderWarning(data), renderOutcome(data), ...renderFailure(data), renderInstance(data), renderArtifacts(data)];
5
11
  return sections.filter((section) => section).join('\n\n');
6
12
  }
7
13
  export function writeArtifacts(dir, snapshot) {
@@ -9,29 +15,25 @@ export function writeArtifacts(dir, snapshot) {
9
15
  const paths = {
10
16
  aria: path.resolve(dir, 'aria.yml'),
11
17
  html: path.resolve(dir, 'page.html'),
12
- network: path.resolve(dir, 'network.jsonl'),
13
18
  };
14
19
  writeFileSync(paths.aria, snapshot.aria ?? '', 'utf-8');
15
20
  writeFileSync(paths.html, snapshot.html ?? '', 'utf-8');
21
+ if (snapshot.screenshot) {
22
+ paths.screenshot = path.resolve(dir, 'page.png');
23
+ writeFileSync(paths.screenshot, snapshot.screenshot);
24
+ }
25
+ if (!snapshot.requests.length)
26
+ return paths;
27
+ paths.network = path.resolve(dir, 'network.jsonl');
16
28
  writeFileSync(paths.network, snapshot.requests.map((request) => `${JSON.stringify(request)}\n`).join(''), 'utf-8');
17
29
  return paths;
18
30
  }
19
31
  function renderResult(data) {
20
32
  const lines = [`ok: ${data.ok}`, `command: ${data.command}`];
21
- const healed = renderHealed(data);
22
- if (healed)
23
- lines.push(healed);
24
33
  if (data.used?.length)
25
34
  lines.push(`used: ${data.used.join('; ')}`);
26
35
  return section('Result', lines.join('\n'));
27
36
  }
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
37
  function renderPage(data) {
36
38
  const { url, previousUrl, title, state, visits } = data.page;
37
39
  const urlLabel = `url: ${url}`;
@@ -43,67 +45,115 @@ function renderPage(data) {
43
45
  const lines = [align(urlLabel, changedMarker, width), `title: ${title}`, align(stateLabel, `(visit #${visits})`, width)];
44
46
  return section('Page', lines.join('\n'));
45
47
  }
48
+ function renderValue(data) {
49
+ if (data.value === undefined)
50
+ return null;
51
+ return section('Value', data.value);
52
+ }
53
+ function renderChanges(data) {
54
+ if (data.changes === undefined || data.changes === null)
55
+ return null;
56
+ return section('Changes', data.changes);
57
+ }
58
+ function renderSteps(data) {
59
+ if (!data.steps?.length)
60
+ return null;
61
+ const lines = [];
62
+ data.steps.forEach((step, index) => {
63
+ let mark = 'FAIL';
64
+ if (step.ok)
65
+ mark = 'ok ';
66
+ if (step.unconfirmed)
67
+ mark = '?? ';
68
+ lines.push(`${index + 1}. ${mark} ${step.label}`);
69
+ for (const line of (step.proof || '').split('\n').filter(Boolean))
70
+ lines.push(` ${line}`);
71
+ });
72
+ if (data.stepFiles)
73
+ lines.push('', `page after each step: ${data.stepFiles}`);
74
+ return section('Steps', lines.join('\n'));
75
+ }
76
+ function renderExpectations(data) {
77
+ if (!data.expectations?.length)
78
+ return null;
79
+ const lines = [];
80
+ data.expectations.forEach((expectation, index) => {
81
+ lines.push(`${index + 1}. ${EXPECTATION_LABELS[expectation.status]} ${expectation.text}`);
82
+ if (expectation.status !== 'contradiction' && expectation.status !== 'failed')
83
+ return;
84
+ for (const line of (expectation.evidence || '').split('\n').filter(Boolean))
85
+ lines.push(` ${line}`);
86
+ });
87
+ return section('Expected outcomes', lines.join('\n'));
88
+ }
89
+ function renderWarning(data) {
90
+ if (!data.warning)
91
+ return null;
92
+ return section('Warning', data.warning);
93
+ }
46
94
  function renderOutcome(data) {
47
- if (data.changes)
48
- return section('Changes', data.changes);
49
95
  if (data.answer)
50
96
  return section('Answer', data.answer);
51
97
  if (data.research)
52
98
  return section('Research', data.research);
53
- if (!data.verdict)
99
+ if (!data.assertions)
54
100
  return null;
55
- const lines = [`passed: ${data.verdict.passed}`, `evidence: ${data.verdict.evidence}`, `code: ${data.verdict.code}`];
56
- return section('Verdict', lines.join('\n'));
101
+ if (!data.assertions.length)
102
+ return section('Assertions', 'none ran — no assertion could express this claim, so nothing was checked against the page');
103
+ const lines = data.assertions.map((assertion) => {
104
+ const code = assertion.code
105
+ .split('\n')
106
+ .map((line) => line.trim())
107
+ .filter((line) => line && !line.startsWith('//'))
108
+ .join(' ');
109
+ return `${code} => ${assertion.passed ? 'PASSED' : 'FAILED'}`;
110
+ });
111
+ const proof = data.assertions.flatMap((assertion) => assertion.proof);
112
+ if (proof.length)
113
+ lines.push('', 'playwright:', ...proof);
114
+ return section('Assertions', lines.join('\n'));
57
115
  }
58
116
  function renderFailure(data) {
59
117
  if (!data.failure)
60
118
  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'));
119
+ return [section('Failure', `error: ${data.failure.error}`), renderCompactAria(data.failure.compactAria)];
73
120
  }
74
121
  function renderCompactAria(compactAria) {
75
122
  if (!compactAria)
76
123
  return null;
77
124
  return section('Current page (compact ARIA)', compactAria);
78
125
  }
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(', ');
126
+ function renderInstance(data) {
127
+ const instance = data.instance;
128
+ const parts = [`${instance.name} (${tabsLabel(instance.tabs)})`, browserLine(instance)];
129
+ if (instance.others.length)
130
+ parts.push(`other instances: ${instance.others.map((other) => `${other.name} (${tabsLabel(other.tabs)})`).join(', ')}`);
131
+ if (data.status)
132
+ parts.push(`details: prima status ${data.status}`);
133
+ return section('Instance', parts.join(' | '));
88
134
  }
89
135
  function browserLine(instance) {
90
136
  if (instance.attached)
91
- return `browser: attached (${instance.attached})`;
137
+ return `attached to ${instance.attached}`;
92
138
  if (instance.startedAgo)
93
- return `browser: running, started ${instance.startedAgo} ago`;
139
+ return `running, started ${instance.startedAgo} ago`;
94
140
  if (instance.tabs > 0)
95
- return 'browser: running';
96
- return 'browser: not running';
141
+ return 'running';
142
+ return 'not running';
97
143
  }
98
144
  function tabsLabel(tabs) {
99
145
  if (tabs === 1)
100
146
  return '1 tab';
101
147
  return `${tabs} tabs`;
102
148
  }
103
- function renderArtifacts(data) {
149
+ export function renderArtifacts(data) {
104
150
  if (!data.artifacts)
105
151
  return null;
106
- const lines = [`aria: ${data.artifacts.aria}`, `html: ${data.artifacts.html}`, `network: ${data.artifacts.network}`];
152
+ const lines = [`aria: ${data.artifacts.aria}`, `html: ${data.artifacts.html}`];
153
+ if (data.artifacts.screenshot)
154
+ lines.push(`screenshot: ${data.artifacts.screenshot}`);
155
+ if (data.artifacts.network)
156
+ lines.push(`network: ${data.artifacts.network}`);
107
157
  return section('Artifacts', lines.join('\n'));
108
158
  }
109
159
  function align(label, marker, width) {