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,83 +2,81 @@ import { Command } from 'commander';
2
2
  import dedent from 'dedent';
3
3
  import { keepServerRunning } from '../../../src/browser-server.ts';
4
4
  import { browserErrorMessage } from '../../../src/utils/browser-errors.ts';
5
- import { setPreserveConsoleLogs } from '../../../src/utils/logger.ts';
5
+ import { isVerboseMode, setQuietMode } from '../../../src/utils/logger.ts';
6
+ import { clearActivityLine, trackActivityLine } from './activity-line.ts';
6
7
  import { type EnvelopeData, renderEnvelope } from './envelope.ts';
7
8
  import { Prima, type PrimaOptions } from './prima.ts';
8
9
 
9
10
  const helpContract = dedent`
10
- Prima drives a browser that is already open. One command per process; every command
11
- prints a plain-text envelope on stdout and exits 0 when ok, 1 when not.
12
-
13
- TIERS - choose by what you hold, not by how hard the step looks
14
- pw <fn> Precise. A Playwright function expression built from a locator you
15
- already verified. No AI on the happy path.
16
- prima pw "({ page }) => page.click('[data-test=submit]')"
17
- click / fill One action described in words; AI resolves it on the current page.
18
- prima click "the primary action button in the header"
19
- prima fill "the search box" "a search term"
20
- do <steps...> Several described steps, run tester-style in one process.
21
- prima do "open the account menu" "choose the settings entry"
22
- Never pass a locator or a function expression to click/fill/do - describe the target.
23
- Never pass a description to pw - it takes executable code only.
24
-
25
- LOOP
26
- prima go <url|path|words> reach the page you want to work on
27
- prima research once per new page; returns verified locators
28
- prima pw "..." drive the page with those locators
29
- prima verify "..." assert the outcome (prima ask "..." to inspect instead)
30
- Fall back to click/fill/do whenever research left you no locator to hold.
31
-
32
- ENVELOPE
33
- ### Result ok, command, healed, used
34
- ### Page url, title, state hash, visit count
35
- ### Changes what the accessibility tree gained or lost
36
- ### Answer | ### Research | ### Verdict output of ask, research, verify
37
- ### Failure error, reasoning, healing attempts, compact ARIA of the page
38
- ### Instance the browser you are on and the other instances running
39
- ### Artifacts paths to the full aria.yml, page.html and network.jsonl
40
- used: is code that already executed - CodeceptJS steps to copy as they are, except
41
- for pw, whose Playwright expression a test needs inside I.usePlaywrightTo(...).
42
- Log lines can precede the envelope; start parsing at the first ### line.
43
-
44
- HEALING AND FAILURE
45
- A failed action is retried by AI along a different route; healed: true means the
46
- outcome was reached another way and used: holds the code that worked.
47
- --no-heal skips that and fails fast.
48
- Failures print compact ARIA inline, so retarget from the envelope itself and open
49
- the artifact files only when the inline snapshot is not enough.
50
-
51
- SESSIONS
52
- By default prima attaches to the playwright-cli browser of this workspace and works
53
- on the tabs it already has open; driving the same session from both tools is the
54
- intended usage.
55
- playwright-cli open <url> the session prima attaches to
56
- --pw-session <title> which playwright-cli session, when several are open
57
- --endpoint <ep> attach to a browser server endpoint directly
58
- prima browser start a prima-owned browser instead, when no session is open
59
- --instance <name> which prima-owned browser you talk to; parallel work
60
- needs one each
61
- --session [file] cookies and storage persisted across processes; ignored
62
- while attached, the attached session keeps its own
63
- Prima never launches a browser implicitly and never closes an attached one - it
64
- disconnects. browser list shows both kinds; ### Instance names the one you are on.
65
- Every browser is reached over a Playwright browser-server endpoint, which needs the
66
- Node build - run prima as "npx explorbot prima ..." or through the published prima
67
- bin; from source under Bun the connection does not open.
68
- When no AI model is usable pw still works; for everything else drive
69
- playwright-cli directly.
70
- Parsed but not active yet: --framework, so reported code is CodeceptJS whatever
71
- you pass.
11
+ Prima is a high-level AI extension to playwright-cli, driving the browser it has open.
12
+
13
+ playwright-cli open <url> starts the session
14
+ prima <command> ... drives it
15
+ playwright-cli close ends it
16
+
17
+ One call takes a whole job:
18
+
19
+ prima check "a workflow can be created and appears in the list" --expected "the new workflow is listed"
20
+ prima do "open the account menu" "choose the settings entry" "switch the theme to dark" "check it took effect"
21
+ prima pw "({ page }) => page.click('[data-test=submit]')"
22
+ `;
23
+
24
+ const checkHelp = dedent`
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.
27
+ --expected one outcome the run must reach, repeatable for several. Without it the
28
+ scenario text is the single expected outcome. Each comes back under
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.
39
+ Page problems seen on the way appear under ### Answer, not as step failures.
40
+ `;
41
+
42
+ const doHelp = dedent`
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.
46
+ Nothing runs past the last instruction given. A whole remaining sequence in one call is
47
+ what makes this tier cheap.
48
+ `;
49
+
50
+ const verifyHelp = dedent`
51
+ Reports each assertion it could express as PASSED or FAILED with its playwright form,
52
+ and gives no overall verdict - read the lines and decide. "none ran" means the claim
53
+ could not be expressed, which is not the same as false.
72
54
  `;
73
55
 
74
- function buildOptions(options: any): PrimaOptions {
56
+ const reportHelp = dedent`
57
+ Commands are logged as they run, so the report needs no browser and outlives the session.
58
+ The most recent session is reported unless --pw-session names another.
59
+ `;
60
+
61
+ const sessionHelp = dedent`
62
+ --endpoint <ep> attach to a browser server endpoint directly, skipping discovery
63
+ --instance <name> which prima-owned browser you talk to; parallel work needs one each
64
+ --session [file] cookies and storage persisted across processes; ignored while
65
+ attached, since the attached session keeps its own
66
+ --framework parsed but not active yet; reported code is CodeceptJS either way
67
+ DEBUG='explorbot:*' in front of a command prints the log of everything it does.
68
+ When no AI model is usable pw still works; for everything else drive playwright-cli.
69
+ `;
70
+
71
+ let rootOptions: () => any = () => ({});
72
+
73
+ function buildOptions(subcommand: any): PrimaOptions {
74
+ const options = { ...rootOptions(), ...stripEmpty(subcommand) };
75
75
  return {
76
- verbose: options.verbose || options.debug,
77
76
  config: options.config,
78
77
  path: options.path,
79
78
  instance: options.instance,
80
79
  session: options.session,
81
- heal: options.heal,
82
80
  ephemeral: options.ephemeral,
83
81
  framework: options.framework,
84
82
  noVision: options.vision === false,
@@ -91,30 +89,39 @@ function buildOptions(options: any): PrimaOptions {
91
89
  };
92
90
  }
93
91
 
92
+ function stripEmpty(options: any): any {
93
+ const present: any = {};
94
+ for (const [key, value] of Object.entries(options || {})) {
95
+ if (value === undefined) continue;
96
+ present[key] = value;
97
+ }
98
+ return present;
99
+ }
100
+
94
101
  function addCommonOptions(cmd: Command): Command {
95
102
  return cmd
96
- .option('-v, --verbose', 'Enable verbose logging')
97
- .option('--debug', 'Enable debug logging (same as --verbose)')
98
103
  .option('-c, --config <path>', 'Path to explorbot configuration file')
99
104
  .option('-p, --path <path>', 'Working directory path')
100
105
  .option('-i, --instance <name>', 'Browser instance to drive')
101
106
  .option('--session [file]', 'Persist cookies and storage to a session file')
102
- .option('--no-heal', 'Fail immediately instead of letting AI retry a failed action')
103
107
  .option('--ephemeral', 'Keep no state between runs; applies to config-free runs, where output goes to a temp directory')
104
108
  .option('--framework <name>', 'Not active yet: framework the reported code targets, codeceptjs or playwright')
105
109
  .option('--url <url>', 'Page to open when the session has no page yet')
106
110
  .option('--endpoint <ep>', 'Websocket endpoint of a browser server to attach to, skipping discovery')
107
- .option('--pw-session <title>', 'Title of the playwright-cli session to attach to');
111
+ .option('--pw-session <title>', 'Title of the playwright-cli session to attach to')
112
+ .addHelpText('after', `\n${sessionHelp}`);
108
113
  }
109
114
 
110
115
  function primaFor(options: any): Prima {
111
- setPreserveConsoleLogs(true);
112
116
  if (options.ephemeral) process.env.EXPLORBOT_EPHEMERAL = '1';
113
117
  return new Prima(buildOptions(options));
114
118
  }
115
119
 
116
- async function runPrima(options: any, command: string, run: (prima: Prima) => Promise<EnvelopeData>): Promise<void> {
120
+ async function runPrima(options: any, command: string, run: (prima: Prima) => Promise<EnvelopeData>, record = true): Promise<void> {
121
+ setQuietMode(!isVerboseMode());
122
+ trackActivityLine();
117
123
  const prima = primaFor(options);
124
+ const startedAt = Date.now();
118
125
 
119
126
  let envelope: EnvelopeData;
120
127
  try {
@@ -124,6 +131,8 @@ async function runPrima(options: any, command: string, run: (prima: Prima) => Pr
124
131
  envelope = await prima.toolFailureEnvelope(command, error);
125
132
  }
126
133
 
134
+ if (record) prima.record(envelope, Date.now() - startedAt);
135
+ clearActivityLine();
127
136
  console.log(renderEnvelope(envelope));
128
137
  await prima.stop().catch(() => {});
129
138
  process.exit(envelope.ok ? 0 : 1);
@@ -143,32 +152,38 @@ async function runBrowser(options: any, run: (prima: Prima) => Promise<boolean>)
143
152
 
144
153
  export function createPrimaCommands(name = 'prima'): Command {
145
154
  const cmd = new Command(name);
146
- cmd.description('Drive an already-open browser one command at a time and report back in a plain-text envelope');
155
+ 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');
156
+ cmd.option('--pw-session <title>', 'Title of the playwright-cli session to attach to');
157
+ cmd.option('--url <url>', 'Page to open when the session has no page yet');
147
158
  cmd.addHelpText('after', `\n${helpContract}`);
159
+ rootOptions = () => cmd.opts();
148
160
 
149
161
  addCommonOptions(cmd.command('pw <fn>').description('Run a Playwright function expression against the open page')).action(async (fn, options) => {
150
162
  await runPrima(options, `pw ${fn}`, (prima) => prima.pw(fn));
151
163
  });
152
164
 
153
- addCommonOptions(cmd.command('do <instructions...>').description('Run high-level instructions tester-style, one argument per instruction')).action(async (instructions, options) => {
154
- await runPrima(options, `do ${instructions.join(' ')}`, (prima) => prima.do(instructions));
155
- });
156
-
157
- addCommonOptions(cmd.command('click <target>').description('Click an element described in plain words')).action(async (target, options) => {
158
- await runPrima(options, `click ${target}`, (prima) => prima.click(target));
159
- });
165
+ addCommonOptions(cmd.command('do <instructions...>').description('Run high-level instructions tester-style, one argument per instruction'))
166
+ .addHelpText('after', `\n${doHelp}`)
167
+ .action(async (instructions, options) => {
168
+ await runPrima(options, `do ${instructions.join(' ')}`, (prima) => prima.do(instructions));
169
+ });
160
170
 
161
- addCommonOptions(cmd.command('fill <field> <value>').description('Fill a field described in plain words')).action(async (field, value, options) => {
162
- await runPrima(options, `fill ${field} ${value}`, (prima) => prima.fill(field, value));
163
- });
171
+ 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'))
172
+ .option('--expected <outcome>', 'An outcome the run must reach; repeat the flag for several', (value: string, all: string[]) => [...all, value], [])
173
+ .addHelpText('after', `\n${checkHelp}`)
174
+ .action(async (scenario, options) => {
175
+ await runPrima(options, `check ${scenario}`, (prima) => prima.check(scenario, options.expected));
176
+ });
164
177
 
165
178
  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) => {
166
179
  await runPrima(options, `ask ${question}`, (prima) => prima.ask(question));
167
180
  });
168
181
 
169
- addCommonOptions(cmd.command('verify <assertion>').alias('assert').description('Assert a statement about the current page')).action(async (assertion, options) => {
170
- await runPrima(options, `verify ${assertion}`, (prima) => prima.verify(assertion));
171
- });
182
+ addCommonOptions(cmd.command('verify <assertion>').alias('assert').description('Assert a statement about the current page'))
183
+ .addHelpText('after', `\n${verifyHelp}`)
184
+ .action(async (assertion, options) => {
185
+ await runPrima(options, `verify ${assertion}`, (prima) => prima.verify(assertion));
186
+ });
172
187
 
173
188
  addCommonOptions(
174
189
  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')
@@ -181,6 +196,32 @@ export function createPrimaCommands(name = 'prima'): Command {
181
196
  await runPrima(options, `go ${target}`, (prima) => prima.go(target));
182
197
  });
183
198
 
199
+ addCommonOptions(cmd.command('config').description('Show models, config file and paths used by this run'))
200
+ .option('--json', 'Print the resolved config as JSON')
201
+ .action(async (options) => {
202
+ setQuietMode(!isVerboseMode());
203
+ const prima = primaFor(options);
204
+ console.log(await prima.config(options.json).catch((error: unknown) => browserErrorMessage(error)));
205
+ await prima.stop().catch(() => {});
206
+ process.exit(0);
207
+ });
208
+
209
+ addCommonOptions(cmd.command('status <hash>').description('Show the artifacts and page detail recorded for an earlier command')).action(async (hash, options) => {
210
+ await runPrima(options, `status ${hash}`, (prima) => prima.status(hash), false);
211
+ });
212
+
213
+ addCommonOptions(cmd.command('report').description('Turn every command of a session into one html and markdown report'))
214
+ .addHelpText('after', `\n${reportHelp}`)
215
+ .action(async (options) => {
216
+ setQuietMode(!isVerboseMode());
217
+ console.log(
218
+ await primaFor(options)
219
+ .report()
220
+ .catch((error: unknown) => browserErrorMessage(error))
221
+ );
222
+ process.exit(0);
223
+ });
224
+
184
225
  const browser = cmd.command('browser').description('Manage the browsers prima drives');
185
226
 
186
227
  addCommonOptions(browser.command('start').description('Start a prima-owned browser and hold it open until Ctrl+C'))
@@ -1,6 +1,13 @@
1
1
  import { mkdirSync, writeFileSync } from 'node:fs';
2
2
  import path from 'node:path';
3
3
 
4
+ const EXPECTATION_LABELS = {
5
+ passed: 'PASSED ',
6
+ failed: 'FAILED ',
7
+ unverified: 'not verified ',
8
+ contradiction: 'CONTRADICTION',
9
+ };
10
+
4
11
  export interface InstanceInfo {
5
12
  name: string;
6
13
  tabs: number;
@@ -9,59 +16,58 @@ export interface InstanceInfo {
9
16
  others: Array<{ name: string; tabs: number }>;
10
17
  }
11
18
 
12
- export interface HealAttempt {
13
- code: string;
14
- outcome: string;
15
- }
16
-
17
19
  export interface EnvelopeData {
18
20
  ok: boolean;
19
21
  command: string;
20
- healed?: boolean;
21
- healNote?: string;
22
22
  used?: string[];
23
23
  page: { url: string; previousUrl?: string; title: string; state: string; visits: number };
24
24
  changes?: string | null;
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;
28
+ stepFiles?: string;
29
+ value?: string;
25
30
  answer?: string;
26
31
  research?: string;
27
- verdict?: { passed: boolean; evidence: string; code: string };
28
- failure?: { error: string; attempts: HealAttempt[]; reasoning?: string; compactAria?: string };
32
+ assertions?: Array<{ code: string; passed: boolean; proof: string[] }>;
33
+ failure?: { error: string; compactAria?: string };
29
34
  instance: InstanceInfo;
30
- artifacts?: { aria: string; html: string; network: string };
35
+ status?: string;
36
+ artifacts?: { aria: string; html: string; screenshot?: string; network?: string };
31
37
  }
32
38
 
33
39
  export function renderEnvelope(data: EnvelopeData): string {
34
- const sections = [renderResult(data), renderPage(data), renderOutcome(data), ...renderFailure(data), renderInstance(data.instance), 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)];
35
41
  return sections.filter((section) => section).join('\n\n');
36
42
  }
37
43
 
38
- 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 } {
39
45
  mkdirSync(dir, { recursive: true });
40
- const paths = {
46
+ const paths: { aria: string; html: string; screenshot?: string; network?: string } = {
41
47
  aria: path.resolve(dir, 'aria.yml'),
42
48
  html: path.resolve(dir, 'page.html'),
43
- network: path.resolve(dir, 'network.jsonl'),
44
49
  };
45
50
  writeFileSync(paths.aria, snapshot.aria ?? '', 'utf-8');
46
51
  writeFileSync(paths.html, snapshot.html ?? '', 'utf-8');
52
+
53
+ if (snapshot.screenshot) {
54
+ paths.screenshot = path.resolve(dir, 'page.png');
55
+ writeFileSync(paths.screenshot, snapshot.screenshot);
56
+ }
57
+
58
+ if (!snapshot.requests.length) return paths;
59
+
60
+ paths.network = path.resolve(dir, 'network.jsonl');
47
61
  writeFileSync(paths.network, snapshot.requests.map((request) => `${JSON.stringify(request)}\n`).join(''), 'utf-8');
48
62
  return paths;
49
63
  }
50
64
 
51
65
  function renderResult(data: EnvelopeData): string {
52
66
  const lines = [`ok: ${data.ok}`, `command: ${data.command}`];
53
- const healed = renderHealed(data);
54
- if (healed) lines.push(healed);
55
67
  if (data.used?.length) lines.push(`used: ${data.used.join('; ')}`);
56
68
  return section('Result', lines.join('\n'));
57
69
  }
58
70
 
59
- function renderHealed(data: EnvelopeData): string | null {
60
- if (data.healed === undefined) return null;
61
- if (data.healNote) return `healed: ${data.healed} (${data.healNote})`;
62
- return `healed: ${data.healed}`;
63
- }
64
-
65
71
  function renderPage(data: EnvelopeData): string {
66
72
  const { url, previousUrl, title, state, visits } = data.page;
67
73
  const urlLabel = `url: ${url}`;
@@ -73,28 +79,73 @@ function renderPage(data: EnvelopeData): string {
73
79
  return section('Page', lines.join('\n'));
74
80
  }
75
81
 
82
+ function renderValue(data: EnvelopeData): string | null {
83
+ if (data.value === undefined) return null;
84
+ return section('Value', data.value);
85
+ }
86
+
87
+ function renderChanges(data: EnvelopeData): string | null {
88
+ if (data.changes === undefined || data.changes === null) return null;
89
+ return section('Changes', data.changes);
90
+ }
91
+
92
+ function renderSteps(data: EnvelopeData): string | null {
93
+ if (!data.steps?.length) return null;
94
+
95
+ const lines: string[] = [];
96
+ data.steps.forEach((step, index) => {
97
+ let mark = 'FAIL';
98
+ if (step.ok) mark = 'ok ';
99
+ if (step.unconfirmed) mark = '?? ';
100
+ lines.push(`${index + 1}. ${mark} ${step.label}`);
101
+ for (const line of (step.proof || '').split('\n').filter(Boolean)) lines.push(` ${line}`);
102
+ });
103
+ if (data.stepFiles) lines.push('', `page after each step: ${data.stepFiles}`);
104
+ return section('Steps', lines.join('\n'));
105
+ }
106
+
107
+ function renderExpectations(data: EnvelopeData): string | null {
108
+ if (!data.expectations?.length) return null;
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
+ });
116
+ return section('Expected outcomes', lines.join('\n'));
117
+ }
118
+
119
+ function renderWarning(data: EnvelopeData): string | null {
120
+ if (!data.warning) return null;
121
+ return section('Warning', data.warning);
122
+ }
123
+
76
124
  function renderOutcome(data: EnvelopeData): string | null {
77
- if (data.changes) return section('Changes', data.changes);
78
125
  if (data.answer) return section('Answer', data.answer);
79
126
  if (data.research) return section('Research', data.research);
80
- if (!data.verdict) return null;
81
- const lines = [`passed: ${data.verdict.passed}`, `evidence: ${data.verdict.evidence}`, `code: ${data.verdict.code}`];
82
- return section('Verdict', lines.join('\n'));
127
+ if (!data.assertions) return null;
128
+
129
+ if (!data.assertions.length) return section('Assertions', 'none ran — no assertion could express this claim, so nothing was checked against the page');
130
+
131
+ const lines = data.assertions.map((assertion) => {
132
+ const code = assertion.code
133
+ .split('\n')
134
+ .map((line) => line.trim())
135
+ .filter((line) => line && !line.startsWith('//'))
136
+ .join(' ');
137
+ return `${code} => ${assertion.passed ? 'PASSED' : 'FAILED'}`;
138
+ });
139
+
140
+ const proof = data.assertions.flatMap((assertion) => assertion.proof);
141
+ if (proof.length) lines.push('', 'playwright:', ...proof);
142
+
143
+ return section('Assertions', lines.join('\n'));
83
144
  }
84
145
 
85
146
  function renderFailure(data: EnvelopeData): Array<string | null> {
86
147
  if (!data.failure) return [];
87
- const lines = [`error: ${data.failure.error}`];
88
- if (data.failure.reasoning) lines.push(`reasoning: ${data.failure.reasoning}`);
89
- return [section('Failure', lines.join('\n')), renderAttempts(data.failure.attempts), renderCompactAria(data.failure.compactAria)];
90
- }
91
-
92
- function renderAttempts(attempts: HealAttempt[]): string | null {
93
- if (!attempts?.length) return null;
94
- const labels = attempts.map((attempt, index) => `${index + 1}. ${attempt.code}`);
95
- const width = Math.max(...labels.map((label) => label.length)) + 3;
96
- const lines = labels.map((label, index) => align(label, `→ ${attempts[index].outcome}`, width));
97
- return section(`Healing attempts (${attempts.length})`, lines.join('\n'));
148
+ return [section('Failure', `error: ${data.failure.error}`), renderCompactAria(data.failure.compactAria)];
98
149
  }
99
150
 
100
151
  function renderCompactAria(compactAria?: string): string | null {
@@ -102,22 +153,19 @@ function renderCompactAria(compactAria?: string): string | null {
102
153
  return section('Current page (compact ARIA)', compactAria);
103
154
  }
104
155
 
105
- function renderInstance(instance: InstanceInfo): string {
106
- const others = instance.others.map((other) => `${other.name} (${tabsLabel(other.tabs)})`);
107
- const lines = [`instance: ${instance.name} (${tabsLabel(instance.tabs)}) | other instances: ${otherInstances(others)}`, browserLine(instance)];
108
- return section('Instance', lines.join('\n'));
109
- }
110
-
111
- function otherInstances(others: string[]): string {
112
- if (!others.length) return 'none';
113
- return others.join(', ');
156
+ function renderInstance(data: EnvelopeData): string | null {
157
+ const instance = data.instance;
158
+ const parts = [`${instance.name} (${tabsLabel(instance.tabs)})`, browserLine(instance)];
159
+ if (instance.others.length) parts.push(`other instances: ${instance.others.map((other) => `${other.name} (${tabsLabel(other.tabs)})`).join(', ')}`);
160
+ if (data.status) parts.push(`details: prima status ${data.status}`);
161
+ return section('Instance', parts.join(' | '));
114
162
  }
115
163
 
116
164
  function browserLine(instance: InstanceInfo): string {
117
- if (instance.attached) return `browser: attached (${instance.attached})`;
118
- if (instance.startedAgo) return `browser: running, started ${instance.startedAgo} ago`;
119
- if (instance.tabs > 0) return 'browser: running';
120
- return 'browser: not running';
165
+ if (instance.attached) return `attached to ${instance.attached}`;
166
+ if (instance.startedAgo) return `running, started ${instance.startedAgo} ago`;
167
+ if (instance.tabs > 0) return 'running';
168
+ return 'not running';
121
169
  }
122
170
 
123
171
  function tabsLabel(tabs: number): string {
@@ -125,9 +173,11 @@ function tabsLabel(tabs: number): string {
125
173
  return `${tabs} tabs`;
126
174
  }
127
175
 
128
- function renderArtifacts(data: EnvelopeData): string | null {
176
+ export function renderArtifacts(data: EnvelopeData): string | null {
129
177
  if (!data.artifacts) return null;
130
- const lines = [`aria: ${data.artifacts.aria}`, `html: ${data.artifacts.html}`, `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}`);
131
181
  return section('Artifacts', lines.join('\n'));
132
182
  }
133
183