cbrowser 20.2.2 → 21.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (302) hide show
  1. package/README.md +2 -2
  2. package/dist/analysis/accessibility-empathy.d.ts +29 -1
  3. package/dist/analysis/accessibility-empathy.d.ts.map +1 -1
  4. package/dist/analysis/accessibility-empathy.js +69 -35
  5. package/dist/analysis/accessibility-empathy.js.map +1 -1
  6. package/dist/browser.d.ts.map +1 -1
  7. package/dist/browser.js +11 -7
  8. package/dist/browser.js.map +1 -1
  9. package/dist/cif-score.d.ts +56 -5
  10. package/dist/cif-score.d.ts.map +1 -1
  11. package/dist/cif-score.js +595 -346
  12. package/dist/cif-score.js.map +1 -1
  13. package/dist/cli.js +10 -8
  14. package/dist/cli.js.map +1 -1
  15. package/dist/cognitive/default-traits.d.ts +14 -0
  16. package/dist/cognitive/default-traits.d.ts.map +1 -0
  17. package/dist/cognitive/default-traits.js +36 -0
  18. package/dist/cognitive/default-traits.js.map +1 -0
  19. package/dist/cognitive/emotions.d.ts.map +1 -1
  20. package/dist/cognitive/emotions.js +104 -174
  21. package/dist/cognitive/emotions.js.map +1 -1
  22. package/dist/cognitive/index.d.ts +28 -0
  23. package/dist/cognitive/index.d.ts.map +1 -1
  24. package/dist/cognitive/index.js +353 -287
  25. package/dist/cognitive/index.js.map +1 -1
  26. package/dist/cognitive/temporal.d.ts +0 -24
  27. package/dist/cognitive/temporal.d.ts.map +1 -1
  28. package/dist/cognitive/temporal.js +45 -35
  29. package/dist/cognitive/temporal.js.map +1 -1
  30. package/dist/data/ctc-reference-v2-2026-10-24.d.ts +16 -0
  31. package/dist/data/ctc-reference-v2-2026-10-24.d.ts.map +1 -0
  32. package/dist/data/ctc-reference-v2-2026-10-24.js +1004 -0
  33. package/dist/data/ctc-reference-v2-2026-10-24.js.map +1 -0
  34. package/dist/ideal-profiles-gap.d.ts +1 -0
  35. package/dist/ideal-profiles-gap.d.ts.map +1 -1
  36. package/dist/ideal-profiles-gap.js +9 -1
  37. package/dist/ideal-profiles-gap.js.map +1 -1
  38. package/dist/ideal-profiles.d.ts +8 -0
  39. package/dist/ideal-profiles.d.ts.map +1 -1
  40. package/dist/ideal-profiles.js +28 -24
  41. package/dist/ideal-profiles.js.map +1 -1
  42. package/dist/mcp-server-remote.d.ts.map +1 -1
  43. package/dist/mcp-server-remote.js +7 -8
  44. package/dist/mcp-server-remote.js.map +1 -1
  45. package/dist/mcp-tools/account-personas.d.ts.map +1 -1
  46. package/dist/mcp-tools/account-personas.js +3 -1
  47. package/dist/mcp-tools/account-personas.js.map +1 -1
  48. package/dist/mcp-tools/base/answer-provenance.d.ts +28 -0
  49. package/dist/mcp-tools/base/answer-provenance.d.ts.map +1 -0
  50. package/dist/mcp-tools/base/answer-provenance.js +32 -0
  51. package/dist/mcp-tools/base/answer-provenance.js.map +1 -0
  52. package/dist/mcp-tools/base/audit-tools.d.ts +0 -3
  53. package/dist/mcp-tools/base/audit-tools.d.ts.map +1 -1
  54. package/dist/mcp-tools/base/audit-tools.js +152 -84
  55. package/dist/mcp-tools/base/audit-tools.js.map +1 -1
  56. package/dist/mcp-tools/base/capture-tools.js +1 -1
  57. package/dist/mcp-tools/base/capture-tools.js.map +1 -1
  58. package/dist/mcp-tools/base/chain-values.d.ts +38 -0
  59. package/dist/mcp-tools/base/chain-values.d.ts.map +1 -0
  60. package/dist/mcp-tools/base/chain-values.js +85 -0
  61. package/dist/mcp-tools/base/chain-values.js.map +1 -0
  62. package/dist/mcp-tools/base/cognitive-tools.d.ts.map +1 -1
  63. package/dist/mcp-tools/base/cognitive-tools.js +126 -120
  64. package/dist/mcp-tools/base/cognitive-tools.js.map +1 -1
  65. package/dist/mcp-tools/base/gif-tools.d.ts.map +1 -1
  66. package/dist/mcp-tools/base/gif-tools.js +23 -5
  67. package/dist/mcp-tools/base/gif-tools.js.map +1 -1
  68. package/dist/mcp-tools/base/persona-comparison-tools.d.ts.map +1 -1
  69. package/dist/mcp-tools/base/persona-comparison-tools.js +229 -172
  70. package/dist/mcp-tools/base/persona-comparison-tools.js.map +1 -1
  71. package/dist/mcp-tools/base/site-familiarity.d.ts +31 -0
  72. package/dist/mcp-tools/base/site-familiarity.d.ts.map +1 -0
  73. package/dist/mcp-tools/base/site-familiarity.js +30 -0
  74. package/dist/mcp-tools/base/site-familiarity.js.map +1 -0
  75. package/dist/mcp-tools/base/site-knowledge-tools.js +1 -1
  76. package/dist/mcp-tools/base/site-knowledge-tools.js.map +1 -1
  77. package/dist/mcp-tools/base/story-narrative.d.ts +50 -0
  78. package/dist/mcp-tools/base/story-narrative.d.ts.map +1 -0
  79. package/dist/mcp-tools/base/story-narrative.js +88 -0
  80. package/dist/mcp-tools/base/story-narrative.js.map +1 -0
  81. package/dist/mcp-tools/base/values-tools.d.ts.map +1 -1
  82. package/dist/mcp-tools/base/values-tools.js +19 -5
  83. package/dist/mcp-tools/base/values-tools.js.map +1 -1
  84. package/dist/mcp-tools/base/visual-testing-tools.d.ts +5 -5
  85. package/dist/mcp-tools/base/visual-testing-tools.d.ts.map +1 -1
  86. package/dist/mcp-tools/base/visual-testing-tools.js +38 -11
  87. package/dist/mcp-tools/base/visual-testing-tools.js.map +1 -1
  88. package/dist/mcp-tools/persona-creation-tools.js +6 -6
  89. package/dist/mcp-tools/persona-creation-tools.js.map +1 -1
  90. package/dist/mcp-tools/persona-lifecycle-tools.d.ts.map +1 -1
  91. package/dist/mcp-tools/persona-lifecycle-tools.js +5 -1
  92. package/dist/mcp-tools/persona-lifecycle-tools.js.map +1 -1
  93. package/dist/mcp-tools/tier-gate.js +1 -1
  94. package/dist/mcp-tools/tool-result-saver.d.ts +7 -0
  95. package/dist/mcp-tools/tool-result-saver.d.ts.map +1 -1
  96. package/dist/mcp-tools/tool-result-saver.js +11 -0
  97. package/dist/mcp-tools/tool-result-saver.js.map +1 -1
  98. package/dist/mcp-tools/trait-provenance.d.ts +109 -0
  99. package/dist/mcp-tools/trait-provenance.d.ts.map +1 -0
  100. package/dist/mcp-tools/trait-provenance.js +187 -0
  101. package/dist/mcp-tools/trait-provenance.js.map +1 -0
  102. package/dist/mcp-tools/ui-resources.d.ts +6 -7
  103. package/dist/mcp-tools/ui-resources.d.ts.map +1 -1
  104. package/dist/mcp-tools/ui-resources.js +162 -12
  105. package/dist/mcp-tools/ui-resources.js.map +1 -1
  106. package/dist/mcp-tools/widget-kit.d.ts +129 -3
  107. package/dist/mcp-tools/widget-kit.d.ts.map +1 -1
  108. package/dist/mcp-tools/widget-kit.js +507 -28
  109. package/dist/mcp-tools/widget-kit.js.map +1 -1
  110. package/dist/performance/metrics.d.ts.map +1 -1
  111. package/dist/performance/metrics.js +4 -1
  112. package/dist/performance/metrics.js.map +1 -1
  113. package/dist/persona-questionnaire.js +1 -1
  114. package/dist/persona-questionnaire.js.map +1 -1
  115. package/dist/personas.d.ts.map +1 -1
  116. package/dist/personas.js +133 -82
  117. package/dist/personas.js.map +1 -1
  118. package/dist/recording/engine.d.ts.map +1 -1
  119. package/dist/recording/engine.js +5 -2
  120. package/dist/recording/engine.js.map +1 -1
  121. package/dist/recording/saliency-overlay.d.ts +8 -3
  122. package/dist/recording/saliency-overlay.d.ts.map +1 -1
  123. package/dist/recording/saliency-overlay.js +14 -5
  124. package/dist/recording/saliency-overlay.js.map +1 -1
  125. package/dist/recording/types.js +1 -1
  126. package/dist/testing/nl-test-suite.d.ts.map +1 -1
  127. package/dist/testing/nl-test-suite.js +2 -1
  128. package/dist/testing/nl-test-suite.js.map +1 -1
  129. package/dist/trait-reference.d.ts +19 -2
  130. package/dist/trait-reference.d.ts.map +1 -1
  131. package/dist/trait-reference.js +19 -2
  132. package/dist/trait-reference.js.map +1 -1
  133. package/dist/types.d.ts +32 -21
  134. package/dist/types.d.ts.map +1 -1
  135. package/dist/types.js +62 -52
  136. package/dist/types.js.map +1 -1
  137. package/dist/values/persona-completeness.js +1 -1
  138. package/dist/values/persona-completeness.js.map +1 -1
  139. package/dist/visual/ab-comparison.d.ts.map +1 -1
  140. package/dist/visual/ab-comparison.js +4 -1
  141. package/dist/visual/ab-comparison.js.map +1 -1
  142. package/dist/visual/attention-quality.d.ts +1 -1
  143. package/dist/visual/attention-quality.js +1 -1
  144. package/dist/visual/attention-transport.d.ts +74 -10
  145. package/dist/visual/attention-transport.d.ts.map +1 -1
  146. package/dist/visual/attention-transport.js +353 -193
  147. package/dist/visual/attention-transport.js.map +1 -1
  148. package/dist/visual/cognitive-models.d.ts +77 -55
  149. package/dist/visual/cognitive-models.d.ts.map +1 -1
  150. package/dist/visual/cognitive-models.js +231 -104
  151. package/dist/visual/cognitive-models.js.map +1 -1
  152. package/dist/visual/cognitive-transport-chain.d.ts +328 -30
  153. package/dist/visual/cognitive-transport-chain.d.ts.map +1 -1
  154. package/dist/visual/cognitive-transport-chain.js +576 -353
  155. package/dist/visual/cognitive-transport-chain.js.map +1 -1
  156. package/dist/visual/cognitive-transport.d.ts +446 -13
  157. package/dist/visual/cognitive-transport.d.ts.map +1 -1
  158. package/dist/visual/cognitive-transport.js +1085 -245
  159. package/dist/visual/cognitive-transport.js.map +1 -1
  160. package/dist/visual/cross-browser.d.ts.map +1 -1
  161. package/dist/visual/cross-browser.js +8 -6
  162. package/dist/visual/cross-browser.js.map +1 -1
  163. package/dist/visual/ctc-bands.d.ts +91 -0
  164. package/dist/visual/ctc-bands.d.ts.map +1 -0
  165. package/dist/visual/ctc-bands.js +117 -0
  166. package/dist/visual/ctc-bands.js.map +1 -0
  167. package/dist/visual/ctc-reference.d.ts +287 -0
  168. package/dist/visual/ctc-reference.d.ts.map +1 -0
  169. package/dist/visual/ctc-reference.js +376 -0
  170. package/dist/visual/ctc-reference.js.map +1 -0
  171. package/dist/visual/demand-laws.d.ts +145 -0
  172. package/dist/visual/demand-laws.d.ts.map +1 -0
  173. package/dist/visual/demand-laws.js +220 -0
  174. package/dist/visual/demand-laws.js.map +1 -0
  175. package/dist/visual/distance-metrics.d.ts +23 -1
  176. package/dist/visual/distance-metrics.d.ts.map +1 -1
  177. package/dist/visual/distance-metrics.js +49 -18
  178. package/dist/visual/distance-metrics.js.map +1 -1
  179. package/dist/visual/dro-bound.d.ts +220 -0
  180. package/dist/visual/dro-bound.d.ts.map +1 -0
  181. package/dist/visual/dro-bound.js +341 -0
  182. package/dist/visual/dro-bound.js.map +1 -0
  183. package/dist/visual/exact-w1.d.ts +60 -0
  184. package/dist/visual/exact-w1.d.ts.map +1 -0
  185. package/dist/visual/exact-w1.js +247 -0
  186. package/dist/visual/exact-w1.js.map +1 -0
  187. package/dist/visual/human-input.d.ts.map +1 -1
  188. package/dist/visual/human-input.js +45 -41
  189. package/dist/visual/human-input.js.map +1 -1
  190. package/dist/visual/index.d.ts +2 -0
  191. package/dist/visual/index.d.ts.map +1 -1
  192. package/dist/visual/index.js +2 -0
  193. package/dist/visual/index.js.map +1 -1
  194. package/dist/visual/law-times-report.d.ts +49 -0
  195. package/dist/visual/law-times-report.d.ts.map +1 -0
  196. package/dist/visual/law-times-report.js +38 -0
  197. package/dist/visual/law-times-report.js.map +1 -0
  198. package/dist/visual/model-provenance.d.ts +151 -0
  199. package/dist/visual/model-provenance.d.ts.map +1 -0
  200. package/dist/visual/model-provenance.js +334 -0
  201. package/dist/visual/model-provenance.js.map +1 -0
  202. package/dist/visual/normal-cdf.d.ts +14 -0
  203. package/dist/visual/normal-cdf.d.ts.map +1 -0
  204. package/dist/visual/normal-cdf.js +26 -0
  205. package/dist/visual/normal-cdf.js.map +1 -0
  206. package/dist/visual/perceptual-transport.d.ts +28 -4
  207. package/dist/visual/perceptual-transport.d.ts.map +1 -1
  208. package/dist/visual/perceptual-transport.js +66 -32
  209. package/dist/visual/perceptual-transport.js.map +1 -1
  210. package/dist/visual/provenance/attention.d.ts +55 -0
  211. package/dist/visual/provenance/attention.d.ts.map +1 -0
  212. package/dist/visual/provenance/attention.js +188 -0
  213. package/dist/visual/provenance/attention.js.map +1 -0
  214. package/dist/visual/provenance/chain.d.ts +41 -0
  215. package/dist/visual/provenance/chain.d.ts.map +1 -0
  216. package/dist/visual/provenance/chain.js +66 -0
  217. package/dist/visual/provenance/chain.js.map +1 -0
  218. package/dist/visual/provenance/cif.d.ts +42 -0
  219. package/dist/visual/provenance/cif.d.ts.map +1 -0
  220. package/dist/visual/provenance/cif.js +116 -0
  221. package/dist/visual/provenance/cif.js.map +1 -0
  222. package/dist/visual/provenance/ctc-reference.d.ts +36 -0
  223. package/dist/visual/provenance/ctc-reference.d.ts.map +1 -0
  224. package/dist/visual/provenance/ctc-reference.js +96 -0
  225. package/dist/visual/provenance/ctc-reference.js.map +1 -0
  226. package/dist/visual/provenance/distance.d.ts +10 -0
  227. package/dist/visual/provenance/distance.d.ts.map +1 -0
  228. package/dist/visual/provenance/distance.js +72 -0
  229. package/dist/visual/provenance/distance.js.map +1 -0
  230. package/dist/visual/provenance/dro.d.ts +10 -0
  231. package/dist/visual/provenance/dro.d.ts.map +1 -0
  232. package/dist/visual/provenance/dro.js +2 -0
  233. package/dist/visual/provenance/dro.js.map +1 -0
  234. package/dist/visual/provenance/emotion.d.ts +26 -0
  235. package/dist/visual/provenance/emotion.d.ts.map +1 -0
  236. package/dist/visual/provenance/emotion.js +99 -0
  237. package/dist/visual/provenance/emotion.js.map +1 -0
  238. package/dist/visual/provenance/human-input.d.ts +19 -0
  239. package/dist/visual/provenance/human-input.d.ts.map +1 -0
  240. package/dist/visual/provenance/human-input.js +84 -0
  241. package/dist/visual/provenance/human-input.js.map +1 -0
  242. package/dist/visual/provenance/journey-group.d.ts +15 -0
  243. package/dist/visual/provenance/journey-group.d.ts.map +1 -0
  244. package/dist/visual/provenance/journey-group.js +15 -0
  245. package/dist/visual/provenance/journey-group.js.map +1 -0
  246. package/dist/visual/provenance/journey-mcp.d.ts +24 -0
  247. package/dist/visual/provenance/journey-mcp.d.ts.map +1 -0
  248. package/dist/visual/provenance/journey-mcp.js +54 -0
  249. package/dist/visual/provenance/journey-mcp.js.map +1 -0
  250. package/dist/visual/provenance/journey.d.ts +34 -0
  251. package/dist/visual/provenance/journey.d.ts.map +1 -0
  252. package/dist/visual/provenance/journey.js +184 -0
  253. package/dist/visual/provenance/journey.js.map +1 -0
  254. package/dist/visual/provenance/laws.d.ts +38 -0
  255. package/dist/visual/provenance/laws.d.ts.map +1 -0
  256. package/dist/visual/provenance/laws.js +40 -0
  257. package/dist/visual/provenance/laws.js.map +1 -0
  258. package/dist/visual/provenance/page-metrics.d.ts +42 -0
  259. package/dist/visual/provenance/page-metrics.d.ts.map +1 -0
  260. package/dist/visual/provenance/page-metrics.js +79 -0
  261. package/dist/visual/provenance/page-metrics.js.map +1 -0
  262. package/dist/visual/provenance/perceptual.d.ts +63 -0
  263. package/dist/visual/provenance/perceptual.d.ts.map +1 -0
  264. package/dist/visual/provenance/perceptual.js +115 -0
  265. package/dist/visual/provenance/perceptual.js.map +1 -0
  266. package/dist/visual/provenance/persona-rates.d.ts +84 -0
  267. package/dist/visual/provenance/persona-rates.d.ts.map +1 -0
  268. package/dist/visual/provenance/persona-rates.js +112 -0
  269. package/dist/visual/provenance/persona-rates.js.map +1 -0
  270. package/dist/visual/provenance/personas.d.ts +122 -0
  271. package/dist/visual/provenance/personas.d.ts.map +1 -0
  272. package/dist/visual/provenance/personas.js +298 -0
  273. package/dist/visual/provenance/personas.js.map +1 -0
  274. package/dist/visual/provenance/profiles.d.ts +39 -0
  275. package/dist/visual/provenance/profiles.d.ts.map +1 -0
  276. package/dist/visual/provenance/profiles.js +37 -0
  277. package/dist/visual/provenance/profiles.js.map +1 -0
  278. package/dist/visual/provenance/temporal.d.ts +21 -0
  279. package/dist/visual/provenance/temporal.d.ts.map +1 -0
  280. package/dist/visual/provenance/temporal.js +56 -0
  281. package/dist/visual/provenance/temporal.js.map +1 -0
  282. package/dist/visual/provenance/visual-distance.d.ts +22 -0
  283. package/dist/visual/provenance/visual-distance.d.ts.map +1 -0
  284. package/dist/visual/provenance/visual-distance.js +30 -0
  285. package/dist/visual/provenance/visual-distance.js.map +1 -0
  286. package/dist/visual/regression.d.ts.map +1 -1
  287. package/dist/visual/regression.js +14 -9
  288. package/dist/visual/regression.js.map +1 -1
  289. package/dist/visual/responsive.d.ts.map +1 -1
  290. package/dist/visual/responsive.js +4 -1
  291. package/dist/visual/responsive.js.map +1 -1
  292. package/dist/visual/scope-prose.js +2 -2
  293. package/dist/visual/scope-prose.js.map +1 -1
  294. package/dist/visual/seeded-random.d.ts +16 -0
  295. package/dist/visual/seeded-random.d.ts.map +1 -0
  296. package/dist/visual/seeded-random.js +25 -0
  297. package/dist/visual/seeded-random.js.map +1 -0
  298. package/dist/visual/stored-accessibility-traits.d.ts +27 -0
  299. package/dist/visual/stored-accessibility-traits.d.ts.map +1 -0
  300. package/dist/visual/stored-accessibility-traits.js +35 -0
  301. package/dist/visual/stored-accessibility-traits.js.map +1 -0
  302. package/package.json +1 -1
@@ -31,7 +31,12 @@ import { isAgentPersonaObject } from "../agent-personas.js";
31
31
  import { createInitialEmotionalState, createEmotionalConfig, applyEmotionalTrigger, decayEmotionsWithTraits, deriveSimplifiedState, calculateAbandonmentModifier, describeEmotionalState, shouldConsiderAbandonment, } from "./emotions.js";
32
32
  import { calculateFatigueIncrement, calculateFittsMovementTime, shouldSwitchToSystem2, canReturnToSystem1, calculateTypingTime, calculateScanWidthMultiplier, calculateGazeMouseLag, calculatePeripheralVision, createHabituationState, updateHabituationState, classifyUIPattern, } from "../types.js";
33
33
  import { loadConfigFile, getDataDir } from "../config.js";
34
+ import { param } from "../visual/model-provenance.js";
35
+ import { defaultJourneyTraits } from "./default-traits.js";
36
+ import { distanceToScreenCentre, smallerOfWidth } from "../visual/demand-laws.js";
34
37
  import { writeSecretFile } from "../secure-file.js";
38
+ import { uniqueStamp } from "../temp-files.js";
39
+ import { semanticWeightTable } from "../visual/attention-transport.js";
35
40
  import { JourneyTraceWriter, TraceReplayProvider, hashPageState, } from "./journey-trace.js";
36
41
  // ============================================================================
37
42
  // API Key Management
@@ -421,7 +426,12 @@ const MOUSE_OVERLAY_SCRIPT = `(() => {
421
426
  * payload, no dashboard-side overlay positioning. Per-frame compute is
422
427
  * sub-millisecond on modern pages because we're just reading bounding boxes.
423
428
  *
424
- * Config is injected as `window.__cbAttentionConfig` by the journey runner.
429
+ * Config is injected as `window.__cbAttentionConfig` by the journey runner
430
+ * (attentionOverlayInitScript). Every weight and goal constant comes from it,
431
+ * that is from attention-transport's semanticWeightTable; this script holds
432
+ * none of its own (ISC-48.1, 2026-10-22). It is still a simpler model than
433
+ * analyzeAttention: max per cell, max-normalised, no values layer, no
434
+ * perceptual filter, no gaze calibration, and its own element classifier.
425
435
  */
426
436
  const ATTENTION_OVERLAY_SCRIPT = `(() => {
427
437
  if (window.__cbAttentionInstalled || window.__cbAttentionPending) return;
@@ -430,9 +440,11 @@ const ATTENTION_OVERLAY_SCRIPT = `(() => {
430
440
  const install = () => {
431
441
  if (window.__cbAttentionInstalled) return;
432
442
  if (!document.documentElement || !document.body) return;
443
+ // No config, no overlay: drawing with invented weights is what this
444
+ // replaced.
445
+ const config = window.__cbAttentionConfig;
446
+ if (!config) return;
433
447
  window.__cbAttentionInstalled = true;
434
-
435
- const config = window.__cbAttentionConfig || { typeWeights: {}, personaMods: {}, goal: "" };
436
448
  const cellSize = 16;
437
449
 
438
450
  const css = \`
@@ -514,15 +526,16 @@ const ATTENTION_OVERLAY_SCRIPT = `(() => {
514
526
  return 'content';
515
527
  };
516
528
 
517
- // Sum of weight from goal-keyword matches in element text.
529
+ // Goal relevance as computeGoalRelevance scores it, with its constants.
530
+ const stopWords = new Set(config.stopWords || []);
518
531
  const goalWords = (config.goal || '').toLowerCase()
519
- .split(/\\s+/).filter(w => w.length > 2 && !/^(the|a|an|to|for|of|in|on|at|is|it|my|i|me|and|or|how|do|can)$/.test(w));
532
+ .split(/\\s+/).filter(w => w.length >= config.minKeywordLength && !stopWords.has(w));
520
533
  const goalRelevance = (text) => {
521
534
  if (!text || !goalWords.length) return 0;
522
535
  const t = text.toLowerCase();
523
536
  let m = 0;
524
537
  for (const w of goalWords) if (t.includes(w)) m++;
525
- return (m / goalWords.length) * 2.0;
538
+ return (m / goalWords.length) * config.relevanceGain;
526
539
  };
527
540
 
528
541
  let lastCells = null;
@@ -553,9 +566,10 @@ const ATTENTION_OVERLAY_SCRIPT = `(() => {
553
566
  if (cs.display === 'none' || cs.visibility === 'hidden' || cs.opacity === '0') continue;
554
567
 
555
568
  const type = classify(el);
556
- let weight = (config.typeWeights && config.typeWeights[type]) || 0.2;
557
- const mod = (config.personaMods && config.personaMods[type]) || 1.0;
558
- weight *= mod;
569
+ // As buildSemanticMap: an unknown type takes the fallback weight, and
570
+ // a type without a modifier is unmodified.
571
+ let weight = config.typeWeights[type] ?? config.typeFallback;
572
+ weight *= config.personaMods[type] ?? 1;
559
573
  const text = (el.innerText || el.textContent || '').slice(0, 200);
560
574
  const gr = goalRelevance(text);
561
575
  if (gr > 0) weight *= (1 + gr);
@@ -621,6 +635,11 @@ const ATTENTION_OVERLAY_SCRIPT = `(() => {
621
635
  canvas.style.display = visible === false ? 'none' : '';
622
636
  };
623
637
  window.__cbAttentionRefresh = schedule;
638
+ // The raw weights of the last frame (before max-normalisation), so a test
639
+ // can read what was painted rather than decode the canvas.
640
+ window.__cbAttentionCells = () => lastCells
641
+ ? { cols: lastDims.cols, rows: lastDims.rows, cellSize, cells: Array.from(lastCells) }
642
+ : null;
624
643
  };
625
644
 
626
645
  if (typeof window.requestIdleCallback === 'function') {
@@ -632,32 +651,26 @@ const ATTENTION_OVERLAY_SCRIPT = `(() => {
632
651
  /**
633
652
  * Build the JSON config bundle that drives the in-browser attention heatmap.
634
653
  * The browser-side script doesn't have access to the SDK's persona profiles,
635
- * so we precompute the type-weight × persona-modifier table here and ship it.
654
+ * so the weight table is computed here and shipped.
655
+ *
656
+ * The table is attention-transport's own (semanticWeightTable), not a copy.
657
+ * Until 2026-10-22 this function held a hand-written mirror "kept in sync by
658
+ * hand", and it had drifted: navigation still read siteFamiliarity, which the
659
+ * model dropped on 2026-08-05, so a journey given customTraits
660
+ * { siteFamiliarity: 0 } drew navigation at 1.0 while attention_analysis drew
661
+ * it at 0.7. The page script also kept its own fallback weight, goal gain,
662
+ * keyword length and stop words; they now come from here too. (ISC-48.1)
636
663
  */
637
- function buildAttentionConfig(personaName, goal, traits) {
638
- // Mirror of ELEMENT_TYPE_WEIGHTS in attention-transport.ts. Kept in sync by
639
- // hand — small enough that drift hasn't been a problem.
640
- const typeWeights = {
641
- cta: 1.0, heading: 0.8, form: 0.7, search: 0.7,
642
- image: 0.5, navigation: 0.4, link: 0.3,
643
- content: 0.2, decorative: 0.1, price: 0.9, error: 0.9,
644
- };
645
- // Trait-driven modifiers. Mirrors computeTraitModifiers in attention-transport.
646
- const t = (k) => traits?.[k] ?? 0.5;
647
- const personaMods = {
648
- cta: 1.0 + (1.0 - t("patience")) * 0.5,
649
- search: 0.7 + (1.0 - t("patience")) * 0.6,
650
- content: t("readingTendency") > 0.6 ? 0.4 + t("readingTendency") * 0.4 : 0.2 + t("patience") * 0.4,
651
- heading: 0.8 + t("curiosity") * 0.4,
652
- image: 0.5 + t("curiosity") * 0.5,
653
- navigation: 0.4 + (1.0 - t("siteFamiliarity")) * 0.6,
654
- error: 0.5 + (1.0 - t("selfEfficacy")) * 0.5,
655
- price: 0.5 + (1.0 - t("riskTolerance")) * 0.5,
656
- decorative: 0.1 + (1.0 - t("metacognitivePlanning")) * 0.4,
657
- form: 0.5 + t("proceduralFluency") * 0.3,
658
- link: 1.0,
664
+ export function buildAttentionConfig(personaName, goal, traits) {
665
+ return {
666
+ ...semanticWeightTable(personaName, traits),
667
+ goal: goal || "",
668
+ persona: personaName,
659
669
  };
660
- return { typeWeights, personaMods, goal: goal || "", persona: personaName };
670
+ }
671
+ /** The init script that installs the in-page heatmap with `config`. */
672
+ export function attentionOverlayInitScript(config) {
673
+ return `window.__cbAttentionConfig = ${JSON.stringify(config)};` + ATTENTION_OVERLAY_SCRIPT;
661
674
  }
662
675
  // Note: CognitiveJourneyOptions accepts `device` to emulate a specific device
663
676
  // preset (mobile/tablet/desktop or "iPhone 15", etc.). Wired in below alongside
@@ -702,7 +715,7 @@ export async function runCognitiveJourney(options) {
702
715
  (options.replayTrace ? new TraceReplayProvider(options.replayTrace) : liveLLMProvider);
703
716
  const nameSlug = options.name ? journeySlug(options.name) : undefined;
704
717
  const traceWriter = options.trace
705
- ? new JourneyTraceWriter(nameSlug ? { journeyId: `journey-${nameSlug}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}` } : undefined)
718
+ ? new JourneyTraceWriter(nameSlug ? { journeyId: `journey-${nameSlug}-${uniqueStamp()}` } : undefined)
706
719
  : null;
707
720
  const personaObj = resolveJourneyPersona(options.persona, options.customTraits);
708
721
  const profile = getCognitiveProfile(personaObj);
@@ -715,26 +728,31 @@ export async function runCognitiveJourney(options) {
715
728
  // (low-vision-magnified, motor-impairment-tremor) get extra penalty so
716
729
  // their clicks visibly miss more often.
717
730
  const aTraits = personaObj.accessibilityTraits;
718
- const motorBase = traits.proceduralFluency ?? 0.5;
731
+ const motorBase = traits.proceduralFluency ?? param("journey.defaultTrait");
719
732
  const motorPenalty = aTraits?.motorControl !== undefined ? aTraits.motorControl : 1.0;
720
733
  const inputTraits = {
721
- proceduralFluency: traits.proceduralFluency ?? 0.5,
722
- motorPrecision: Math.max(0.1, Math.min(1, motorBase * motorPenalty)),
723
- patience: traits.patience ?? 0.5,
724
- readingTendency: traits.readingTendency ?? 0.5,
734
+ proceduralFluency: traits.proceduralFluency ?? param("journey.defaultTrait"),
735
+ motorPrecision: Math.max(param("journey.input.motorPrecisionFloor"), Math.min(1, motorBase * motorPenalty)),
736
+ patience: traits.patience ?? param("journey.defaultTrait"),
737
+ readingTendency: traits.readingTendency ?? param("journey.defaultTrait"),
725
738
  };
726
739
  // Calculate abandonment thresholds
740
+ // Every limit is registered (journey.abandon.*, src/visual/provenance/journey.ts).
741
+ const lowComprehension = traits.comprehension < param("journey.band.low.comprehension");
742
+ const lowPatience = traits.patience < param("journey.band.low.patience");
743
+ const highPatience = traits.patience > param("journey.band.high.patience");
744
+ const lowPersistence = traits.persistence < param("journey.band.low.persistence");
745
+ const highPersistence = traits.persistence > param("journey.band.high.persistence");
727
746
  const thresholds = {
728
- patienceMin: 0.1,
729
- confusionMax: traits.comprehension < 0.4 ? 0.75 : 0.9,
730
- frustrationMax: traits.patience < 0.3 ? 0.8 : 0.95,
731
- maxStepsWithoutProgress: traits.persistence > 0.7 ? 20 : 15,
732
- loopDetectionThreshold: 3,
747
+ patienceMin: param("journey.abandon.patienceFloor"),
748
+ confusionMax: lowComprehension ? param("journey.abandon.confusionLimit.lowComprehension") : param("journey.abandon.confusionLimit"),
749
+ frustrationMax: lowPatience ? param("journey.abandon.frustrationLimit.lowPatience") : param("journey.abandon.frustrationLimit"),
750
+ maxStepsWithoutProgress: highPersistence ? param("journey.abandon.noProgressSteps.highPersistence") : param("journey.abandon.noProgressSteps"),
733
751
  timeLimit: options.maxTime ||
734
- (traits.patience > 0.7 ? 180 : traits.patience < 0.3 ? 60 : 120),
752
+ (highPatience ? param("journey.abandon.timeLimitSeconds.highPatience") : lowPatience ? param("journey.abandon.timeLimitSeconds.lowPatience") : param("journey.abandon.timeLimitSeconds")),
735
753
  // Decision fatigue threshold based on persistence (v9.9.0)
736
754
  // High persistence = can handle more decisions before giving up
737
- decisionFatigueMax: traits.persistence > 0.7 ? 0.95 : traits.persistence < 0.3 ? 0.7 : 0.85,
755
+ decisionFatigueMax: highPersistence ? param("journey.abandon.fatigueLimit.highPersistence") : lowPersistence ? param("journey.abandon.fatigueLimit.lowPersistence") : param("journey.abandon.fatigueLimit"),
738
756
  };
739
757
  // Initialize state
740
758
  const state = {
@@ -742,7 +760,7 @@ export async function runCognitiveJourney(options) {
742
760
  confusionLevel: 0.0,
743
761
  frustrationLevel: 0.0,
744
762
  goalProgress: 0.0,
745
- confidenceLevel: 0.5,
763
+ confidenceLevel: param("journey.initialConfidence"),
746
764
  currentMood: "neutral",
747
765
  memory: {
748
766
  pagesVisited: [options.startUrl],
@@ -762,8 +780,8 @@ export async function runCognitiveJourney(options) {
762
780
  // Dual-process cognitive mode (v10.0.0)
763
781
  // Experts start in System 1 (fast), novices in System 2 (slow)
764
782
  cognitiveMode: {
765
- system: traits.comprehension > 0.7 ? 1 : 2,
766
- switchThreshold: traits.comprehension < 0.4 ? 0.4 : 0.6,
783
+ system: traits.comprehension > param("journey.band.high.comprehension") ? 1 : 2,
784
+ switchThreshold: lowComprehension ? param("journey.dualProcess.switchThreshold.lowComprehension") : param("journey.dualProcess.switchThreshold"),
767
785
  system1Errors: 0,
768
786
  timeInSystem1: 0,
769
787
  timeInSystem2: 0,
@@ -772,7 +790,7 @@ export async function runCognitiveJourney(options) {
772
790
  scanPattern: {
773
791
  basePattern: personaObj.humanBehavior?.attention?.pattern || "f-pattern",
774
792
  widthMultiplier: 1.0,
775
- effectiveWidth: 100,
793
+ effectiveWidth: param("journey.percentScale"),
776
794
  },
777
795
  missedDueToTunnelVision: 0,
778
796
  // Peripheral vision affected by stress (v10.1.0) - Yerkes-Dodson Law
@@ -784,7 +802,9 @@ export async function runCognitiveJourney(options) {
784
802
  // Habituation/banner blindness state (v10.1.0)
785
803
  habituation: createHabituationState(
786
804
  // Low comprehension = faster blindness (lower threshold)
787
- traits.comprehension > 0.7 ? 5 : traits.comprehension < 0.4 ? 2 : 3),
805
+ traits.comprehension > param("journey.band.high.comprehension")
806
+ ? param("journey.habituation.exposures.highComprehension")
807
+ : lowComprehension ? param("journey.habituation.exposures.lowComprehension") : param("journey.habituation.exposures")),
788
808
  // Gaze-to-mouse lag based on age (v10.1.0) - WebGazer.js research
789
809
  gazeMouseLag: calculateGazeMouseLag(personaObj.demographics?.age_range),
790
810
  // Emotional state based on appraisal theory (v13.1.0) - Scherer 2001
@@ -805,14 +825,15 @@ export async function runCognitiveJourney(options) {
805
825
  if (ageMatch) {
806
826
  const age = parseInt(ageMatch[1], 10);
807
827
  // Motor control degrades with age: 1.0 at 25, 1.5 at 65, 2.0 at 85
808
- ageModifier = age > 40 ? 1 + (age - 40) / 50 : 1.0;
828
+ const onset = param("journey.fitts.ageOnset");
829
+ ageModifier = age > onset ? 1 + (age - onset) / param("journey.fitts.agePerSlowdown") : 1.0;
809
830
  }
810
831
  }
811
832
  // Derive tremor modifier from mouse params or motor traits
812
833
  let tremorModifier = 0;
813
834
  if (mouseParams) {
814
835
  // Higher jitter = more tremor effect
815
- tremorModifier = (mouseParams.jitter || 0) / 10;
836
+ tremorModifier = (mouseParams.jitter || 0) / param("journey.fitts.jitterScale");
816
837
  }
817
838
  const fittsParams = {
818
839
  ageModifier,
@@ -848,14 +869,14 @@ export async function runCognitiveJourney(options) {
848
869
  // Window size stays at 1920×1080 so chromium fills Xvfb edge-to-edge.
849
870
  // Dashboard overlay positions: top:7.87% (85/1080), height:92.13% (995/1080).
850
871
  if (options.enableMouseOverlay) {
851
- browserConfig.viewportWidth = 1920;
852
- browserConfig.viewportHeight = 995;
853
- browserConfig.windowWidth = 1920;
854
- browserConfig.windowHeight = 1080;
872
+ browserConfig.viewportWidth = param("journey.live.screenWidth");
873
+ browserConfig.viewportHeight = param("journey.live.pageHeight");
874
+ browserConfig.windowWidth = param("journey.live.screenWidth");
875
+ browserConfig.windowHeight = param("journey.live.screenHeight");
855
876
  // Heavy university / news / e-commerce sites often need >30s to reach
856
877
  // domcontentloaded with all their analytics/CDN scripts. Live journeys
857
878
  // get a 60s navigation budget per attempt.
858
- browserConfig.timeout = 60000;
879
+ browserConfig.timeout = param("journey.live.navigationTimeoutMs");
859
880
  // When a click opens target="_blank" or window.open, follow the new tab
860
881
  // so VNC keeps showing where the persona actually went.
861
882
  browserConfig.followPopups = true;
@@ -865,7 +886,7 @@ export async function runCognitiveJourney(options) {
865
886
  browserConfig.viewportHeight = options.viewport.height;
866
887
  browserConfig.viewportOverride = options.viewport;
867
888
  }
868
- const browser = new CBrowser(browserConfig);
889
+ const browser = options.createBrowser ? options.createBrowser(browserConfig) : new CBrowser(browserConfig);
869
890
  // Helper that re-injects the mouse overlay. The overlay script is idempotent
870
891
  // (gates on window.__cbHelperInstalled), so calling this after every navigation
871
892
  // is safe and ensures the overlay always survives full page loads.
@@ -892,8 +913,7 @@ export async function runCognitiveJourney(options) {
892
913
  // Persona modifiers (type weights × cognitive traits) are precomputed
893
914
  // here in Node and serialized into the page as window.__cbAttentionConfig.
894
915
  const attentionConfig = buildAttentionConfig(personaObj.name, options.goal, traits);
895
- const configBootstrap = `window.__cbAttentionConfig = ${JSON.stringify(attentionConfig)};`;
896
- await initialPage.context().addInitScript(configBootstrap + ATTENTION_OVERLAY_SCRIPT);
916
+ await initialPage.context().addInitScript(attentionOverlayInitScript(attentionConfig));
897
917
  }
898
918
  catch (e) {
899
919
  console.warn(`[cognitive] Overlay context init failed: ${e.message}`);
@@ -908,7 +928,8 @@ export async function runCognitiveJourney(options) {
908
928
  // Hard timeout per attempt is browserConfig.timeout (set to 60s above for
909
929
  // live mode).
910
930
  let lastErr;
911
- for (let attempt = 1; attempt <= 4; attempt++) {
931
+ const navAttempts = param("journey.nav.attempts");
932
+ for (let attempt = 1; attempt <= navAttempts; attempt++) {
912
933
  try {
913
934
  await browser.navigate(options.startUrl, { waitStrategy: "domcontentloaded" });
914
935
  lastErr = undefined;
@@ -917,11 +938,11 @@ export async function runCognitiveJourney(options) {
917
938
  catch (navErr) {
918
939
  lastErr = navErr;
919
940
  const isTransient = /ERR_NETWORK_CHANGED|ERR_CONNECTION|ERR_TIMED_OUT|Timeout/i.test(lastErr.message);
920
- if (attempt === 4 || !isTransient) {
941
+ if (attempt === navAttempts || !isTransient) {
921
942
  if (!isTransient)
922
943
  throw lastErr;
923
944
  }
924
- await new Promise(r => setTimeout(r, 2500 * attempt));
945
+ await new Promise(r => setTimeout(r, param("journey.nav.backoffMs") * attempt));
925
946
  }
926
947
  }
927
948
  if (lastErr)
@@ -931,7 +952,7 @@ export async function runCognitiveJourney(options) {
931
952
  // Auto-capture (v18.70.0): recording starts once the journey's start URL is
932
953
  // loaded, so frame 0 is the page the persona actually opens on, and stops
933
954
  // just before the browser closes. Capture never fails the journey.
934
- const captureSession = await startAutoCapture(await browser.getPage(), options.capture, `journey-${nameSlug ?? journeySlug(options.persona)}-${Date.now()}`);
955
+ const captureSession = await startAutoCapture(await browser.getPage(), options.capture, `journey-${nameSlug ?? journeySlug(options.persona)}-${uniqueStamp()}`);
935
956
  // Apply geolocation at runtime as well (ensures permission is granted after context exists)
936
957
  if (effectiveLocation.geolocation) {
937
958
  await browser.setGeolocationRuntime(effectiveLocation.geolocation);
@@ -939,7 +960,7 @@ export async function runCognitiveJourney(options) {
939
960
  const fullMonologue = [];
940
961
  const frictionPoints = [];
941
962
  const startTime = Date.now();
942
- const maxSteps = options.maxSteps || 50;
963
+ const maxSteps = options.maxSteps || param("journey.maxSteps");
943
964
  // ── Returning-visit familiarity boost ─────────────────────────────────
944
965
  // The persona's existing siteFamiliarity trait already drives perception
945
966
  // (navigation weighting in attention-transport, scan-path defaults, etc.).
@@ -950,8 +971,8 @@ export async function runCognitiveJourney(options) {
950
971
  // 5 ≈ +0.72, 10+ ≈ +0.92 — asymptotes near 1.
951
972
  if (options.priorMemory && options.priorMemory.visitCount > 0) {
952
973
  const v = options.priorMemory.visitCount;
953
- const familiarityBoost = 1 - Math.exp(-v / 4);
954
- const baseline = traits.siteFamiliarity ?? 0.5;
974
+ const familiarityBoost = 1 - Math.exp(-v / param("journey.familiarity.visitScale"));
975
+ const baseline = traits.siteFamiliarity ?? param("journey.defaultTrait");
955
976
  // Boost toward 1.0 from baseline. The persona keeps any fundamental
956
977
  // cognitive trait (e.g. low transferLearning still matters) but the
957
978
  // domain-specific familiarity component lifts.
@@ -973,7 +994,7 @@ export async function runCognitiveJourney(options) {
973
994
  temporalState = getTemporalState(tCtx);
974
995
  temporalModulators = computeTemporalModulators(temporalState);
975
996
  // Apply trait deltas
976
- traits.patience = Math.max(0, Math.min(1, (traits.patience ?? 0.5) + temporalModulators.patienceDelta));
997
+ traits.patience = Math.max(0, Math.min(1, (traits.patience ?? param("journey.defaultTrait")) + temporalModulators.patienceDelta));
977
998
  if (traits.workingMemory !== undefined) {
978
999
  traits.workingMemory = Math.max(0, Math.min(1, traits.workingMemory + temporalModulators.workingMemoryDelta));
979
1000
  }
@@ -998,7 +1019,7 @@ export async function runCognitiveJourney(options) {
998
1019
  if (options.priorMemory && options.priorMemory.visitCount > 0) {
999
1020
  const m = options.priorMemory;
1000
1021
  const sinceDays = m.lastVisitAt
1001
- ? Math.round((Date.now() - new Date(m.lastVisitAt.replace(" ", "T") + "Z").getTime()) / (1000 * 60 * 60 * 24))
1022
+ ? Math.round((Date.now() - new Date(m.lastVisitAt.replace(" ", "T") + "Z").getTime()) / param("journey.msPerDay"))
1002
1023
  : null;
1003
1024
  const sinceStr = sinceDays === null
1004
1025
  ? "before"
@@ -1061,7 +1082,7 @@ Right now it's ${temporalState.description}.`;
1061
1082
  // Main cognitive loop
1062
1083
  for (let step = 1; step <= maxSteps; step++) {
1063
1084
  state.stepCount = step;
1064
- state.timeElapsed = (Date.now() - startTime) / 1000;
1085
+ state.timeElapsed = (Date.now() - startTime) / param("journey.msPerSecond");
1065
1086
  // Check time limit
1066
1087
  if (state.timeElapsed > thresholds.timeLimit) {
1067
1088
  abandonmentReason = "timeout";
@@ -1095,7 +1116,7 @@ Right now it's ${temporalState.description}.`;
1095
1116
  try {
1096
1117
  await Promise.race([
1097
1118
  page.evaluate("1+1"),
1098
- new Promise((_, rej) => setTimeout(() => rej(new Error("renderer health check timed out")), 5000)),
1119
+ new Promise((_, rej) => setTimeout(() => rej(new Error("renderer health check timed out")), param("journey.healthCheckMs"))),
1099
1120
  ]);
1100
1121
  }
1101
1122
  catch (healthErr) {
@@ -1116,7 +1137,7 @@ Right now it's ${temporalState.description}.`;
1116
1137
  const screenshotKey = currentUrl.split('?')[0]; // strip query for stable keying
1117
1138
  const lastCapturedScrollY = lastScreenshotScroll.get(screenshotKey);
1118
1139
  const isInitialLoadForUrl = lastCapturedScrollY === undefined;
1119
- const scrolledEnough = lastCapturedScrollY !== undefined && Math.abs(viewportCtx.scroll.y - lastCapturedScrollY) > 100;
1140
+ const scrolledEnough = lastCapturedScrollY !== undefined && Math.abs(viewportCtx.scroll.y - lastCapturedScrollY) > param("journey.screenshot.scrollPx");
1120
1141
  const shouldCapture = isInitialLoadForUrl || scrolledEnough;
1121
1142
  let screenshotPath;
1122
1143
  let attentionHeatmapBase64;
@@ -1143,8 +1164,9 @@ Right now it's ${temporalState.description}.`;
1143
1164
  // step 2 onward (step 1 has no prior to compare against).
1144
1165
  if (lastActionOutcome) {
1145
1166
  const currentTexts = new Set(availableElements.map(e => e.text));
1146
- const newClickables = [...currentTexts].filter(t => t && !prevClickableTexts.has(t)).slice(0, 8);
1147
- const removedClickables = [...prevClickableTexts].filter(t => t && !currentTexts.has(t)).slice(0, 8);
1167
+ // Uncapped: the prompt names the first few (journey.prompt.changedElementsShown).
1168
+ const newClickables = [...currentTexts].filter(t => t && !prevClickableTexts.has(t));
1169
+ const removedClickables = [...prevClickableTexts].filter(t => t && !currentTexts.has(t));
1148
1170
  lastActionOutcome.newClickables = newClickables;
1149
1171
  lastActionOutcome.removedClickables = removedClickables;
1150
1172
  // After a navigation the scroll position belongs to the new page, so the
@@ -1162,9 +1184,20 @@ Right now it's ${temporalState.description}.`;
1162
1184
  // omitted the actual prices, and a long article omitted its body. The new
1163
1185
  // version walks the visible viewport and captures every meaningful text
1164
1186
  // node above-the-fold + a band just below, ordered top-to-bottom.
1165
- const pageContent = await page.evaluate(() => {
1187
+ // The extractor runs in the page, which cannot call param(); its limits
1188
+ // are read here and passed in.
1189
+ const contentLimits = {
1190
+ lookaheadShare: param("journey.content.lookaheadShare"),
1191
+ aboveViewportPx: param("journey.content.aboveViewportPx"),
1192
+ minWidthPx: param("journey.content.minWidthPx"),
1193
+ minHeightPx: param("journey.content.minHeightPx"),
1194
+ minChars: param("journey.content.minChars"),
1195
+ maxChars: param("journey.content.maxChars"),
1196
+ maxLines: param("journey.content.maxLines"),
1197
+ };
1198
+ const pageContent = await page.evaluate((L) => {
1166
1199
  const vh = window.innerHeight;
1167
- const lookahead = vh * 0.4; // include a strip just below the fold so AI can plan ahead
1200
+ const lookahead = vh * L.lookaheadShare; // include a strip just below the fold so AI can plan ahead
1168
1201
  const out = [];
1169
1202
  const skipTags = new Set(['SCRIPT', 'STYLE', 'NOSCRIPT', 'svg', 'IFRAME']);
1170
1203
  const nodes = document.querySelectorAll('h1, h2, h3, h4, p, li, td, th, [role="heading"], [data-testid]');
@@ -1172,20 +1205,20 @@ Right now it's ${temporalState.description}.`;
1172
1205
  if (skipTags.has(el.tagName))
1173
1206
  return;
1174
1207
  const r = el.getBoundingClientRect();
1175
- if (r.bottom < -50)
1208
+ if (r.bottom < -L.aboveViewportPx)
1176
1209
  return; // above viewport
1177
1210
  if (r.top > vh + lookahead)
1178
1211
  return; // far below
1179
- if (r.width < 20 || r.height < 8)
1212
+ if (r.width < L.minWidthPx || r.height < L.minHeightPx)
1180
1213
  return;
1181
1214
  const style = window.getComputedStyle(el);
1182
1215
  if (style.display === 'none' || style.visibility === 'hidden')
1183
1216
  return;
1184
1217
  const text = el.innerText?.trim();
1185
- if (!text || text.length < 3)
1218
+ if (!text || text.length < L.minChars)
1186
1219
  return;
1187
1220
  // Trim long paragraphs but keep prices/key info
1188
- const trimmed = text.length > 220 ? text.substring(0, 220) + '…' : text;
1221
+ const trimmed = text.length > L.maxChars ? text.substring(0, L.maxChars) + '…' : text;
1189
1222
  const tag = el.tagName.toLowerCase();
1190
1223
  const tagLabel = ['h1', 'h2', 'h3', 'h4'].includes(tag) ? tag.toUpperCase() : tag;
1191
1224
  const visMark = r.top < vh && r.bottom > 0 ? '👁' : '↓';
@@ -1201,8 +1234,8 @@ Right now it's ${temporalState.description}.`;
1201
1234
  prev = o.line;
1202
1235
  }
1203
1236
  }
1204
- return dedup.slice(0, 30).join('\n');
1205
- }).catch(() => '');
1237
+ return dedup.slice(0, L.maxLines).join('\n');
1238
+ }, contentLimits).catch(() => '');
1206
1239
  // Habituation/Banner Blindness tracking (v10.1.0)
1207
1240
  // Detect UI patterns in available elements and update habituation state
1208
1241
  if (state.habituation) {
@@ -1304,9 +1337,10 @@ Right now it's ${temporalState.description}.`;
1304
1337
  const actionSuccess = parsed.actionSuccess !== false;
1305
1338
  const claudeConfusion = parsed.newConfusion ?? state.emotionalState.confusion;
1306
1339
  const claudeFrustration = parsed.newFrustration ?? state.emotionalState.frustration;
1307
- const confusionIncreased = claudeConfusion > state.emotionalState.confusion + 0.05;
1308
- const confusionDecreased = claudeConfusion < state.emotionalState.confusion - 0.05;
1309
- const frustrationIncreased = claudeFrustration > state.emotionalState.frustration + 0.05;
1340
+ const signalThreshold = param("journey.signal.changeThreshold");
1341
+ const confusionIncreased = claudeConfusion > state.emotionalState.confusion + signalThreshold;
1342
+ const confusionDecreased = claudeConfusion < state.emotionalState.confusion - signalThreshold;
1343
+ const frustrationIncreased = claudeFrustration > state.emotionalState.frustration + signalThreshold;
1310
1344
  // Severity scales with the magnitude of the LLM-reported delta — Claude
1311
1345
  // proposes a value, we use the *delta* (clamped to [0, 1]) as the
1312
1346
  // trigger's intensity, instead of trusting Claude's absolute number.
@@ -1316,7 +1350,7 @@ Right now it's ${temporalState.description}.`;
1316
1350
  // matched signals can fire sequentially below if both are strong.
1317
1351
  const triggersToFire = [];
1318
1352
  if (!actionSuccess && parsed.errorMessage) {
1319
- triggersToFire.push({ trigger: "error", severity: 1.2, description: parsed.errorMessage });
1353
+ triggersToFire.push({ trigger: "error", severity: param("journey.severity.error"), description: parsed.errorMessage });
1320
1354
  }
1321
1355
  else if (!actionSuccess) {
1322
1356
  triggersToFire.push({ trigger: "failure", severity: 1.0, description: "Action did not succeed" });
@@ -1325,7 +1359,7 @@ Right now it's ${temporalState.description}.`;
1325
1359
  if (confusionIncreased) {
1326
1360
  triggersToFire.push({
1327
1361
  trigger: "confusion_onset",
1328
- severity: Math.max(0.5, confusionDelta * 2),
1362
+ severity: Math.max(param("journey.severity.signalFloor"), confusionDelta * param("journey.severity.signalGain")),
1329
1363
  description: parsed.frictionDescription || "UI became confusing",
1330
1364
  });
1331
1365
  }
@@ -1335,7 +1369,7 @@ Right now it's ${temporalState.description}.`;
1335
1369
  // via decay.
1336
1370
  triggersToFire.push({
1337
1371
  trigger: "clarity",
1338
- severity: 0.8,
1372
+ severity: param("journey.severity.clarity"),
1339
1373
  description: "UI became clear",
1340
1374
  });
1341
1375
  }
@@ -1343,24 +1377,24 @@ Right now it's ${temporalState.description}.`;
1343
1377
  // Friction without outright failure
1344
1378
  triggersToFire.push({
1345
1379
  trigger: "setback",
1346
- severity: Math.max(0.5, frustrationDelta * 2),
1380
+ severity: Math.max(param("journey.severity.signalFloor"), frustrationDelta * param("journey.severity.signalGain")),
1347
1381
  description: "Encountered friction",
1348
1382
  });
1349
1383
  }
1350
1384
  // Time pressure fires when patience is already derived-low (recompute
1351
1385
  // briefly here using current state for the gating check).
1352
1386
  const patiencePreview = deriveSimplifiedState(state.emotionalState, traits).patienceRemaining;
1353
- if (patiencePreview < 0.3 && !triggersToFire.length) {
1387
+ if (patiencePreview < param("journey.timePressure.patience") && !triggersToFire.length) {
1354
1388
  triggersToFire.push({
1355
1389
  trigger: "time_pressure",
1356
- severity: 0.8,
1390
+ severity: param("journey.severity.timePressure"),
1357
1391
  description: "Running out of patience",
1358
1392
  });
1359
1393
  }
1360
1394
  if (actionSuccess && !triggersToFire.length) {
1361
1395
  triggersToFire.push({
1362
1396
  trigger: "success",
1363
- severity: 0.8,
1397
+ severity: param("journey.severity.success"),
1364
1398
  description: parsed.action || "Action completed",
1365
1399
  });
1366
1400
  }
@@ -1401,7 +1435,7 @@ Right now it's ${temporalState.description}.`;
1401
1435
  if (wasInteractive && actionSucceeded) {
1402
1436
  // Allow up to the LLM's proposed delta; URL-change reconciliation later
1403
1437
  // expands the cap to full LLM proposal if URL changed.
1404
- progressDeltaAllowed = Math.min(progressDeltaProposed, 0.05);
1438
+ progressDeltaAllowed = Math.min(progressDeltaProposed, param("journey.progress.inPageCap"));
1405
1439
  }
1406
1440
  // No evidence at all → no progress (Claude was hallucinating advancement)
1407
1441
  }
@@ -1466,10 +1500,11 @@ Right now it's ${temporalState.description}.`;
1466
1500
  const cognitiveLoad = (state.confusionLevel + state.frustrationLevel) / 2;
1467
1501
  const prevWidth = state.scanPattern.widthMultiplier;
1468
1502
  state.scanPattern.widthMultiplier = calculateScanWidthMultiplier(cognitiveLoad);
1469
- state.scanPattern.effectiveWidth = state.scanPattern.widthMultiplier * 100;
1503
+ state.scanPattern.effectiveWidth = state.scanPattern.widthMultiplier * param("journey.percentScale");
1470
1504
  // Log if pattern narrowed significantly
1471
1505
  if (options.verbose && prevWidth !== state.scanPattern.widthMultiplier) {
1472
- if (state.scanPattern.widthMultiplier < 0.5) {
1506
+ // The scan width takes only its registered values; the two narrowest are tunnel vision.
1507
+ if (state.scanPattern.widthMultiplier <= param("journey.scan.highWidth")) {
1473
1508
  console.log(`👁️ Tunnel vision: scanning only left ${state.scanPattern.effectiveWidth.toFixed(0)}% of viewport`);
1474
1509
  }
1475
1510
  else if (prevWidth < state.scanPattern.widthMultiplier) {
@@ -1482,8 +1517,9 @@ Right now it's ${temporalState.description}.`;
1482
1517
  const prevArousal = state.peripheralVision.arousalLevel;
1483
1518
  state.peripheralVision = calculatePeripheralVision(state.frustrationLevel, state.confusionLevel);
1484
1519
  // Log significant arousal changes
1485
- if (options.verbose && Math.abs(state.peripheralVision.arousalLevel - prevArousal) > 0.2) {
1486
- if (state.peripheralVision.widthFactor < 0.5) {
1520
+ if (options.verbose && Math.abs(state.peripheralVision.arousalLevel - prevArousal) > param("journey.log.arousalChange")) {
1521
+ // The field takes only its registered factors; the severe one is reported.
1522
+ if (state.peripheralVision.widthFactor <= param("journey.peripheral.severeWidth")) {
1487
1523
  console.log(`😰 Stress tunnel vision: peripheral field reduced to ${(state.peripheralVision.widthFactor * 100).toFixed(0)}%`);
1488
1524
  }
1489
1525
  }
@@ -1516,13 +1552,14 @@ Right now it's ${temporalState.description}.`;
1516
1552
  viewport: { width: viewportCtx.viewport.width, height: viewportCtx.viewport.height },
1517
1553
  });
1518
1554
  // Record friction point if confusion/frustration spiked
1519
- if (parsed.frictionDescription && (state.confusionLevel > 0.4 || state.frustrationLevel > 0.4)) {
1555
+ const frictionLevel = param("journey.friction.level");
1556
+ if (parsed.frictionDescription && (state.confusionLevel > frictionLevel || state.frustrationLevel > frictionLevel)) {
1520
1557
  frictionPoints.push({
1521
1558
  step,
1522
1559
  url: currentUrl,
1523
1560
  element: parsed.frictionElement,
1524
1561
  type: "confusing_ui",
1525
- frustrationIncrease: parsed.newFrustration ? parsed.newFrustration - state.frustrationLevel : 0.1,
1562
+ frustrationIncrease: parsed.newFrustration ? parsed.newFrustration - state.frustrationLevel : param("journey.friction.defaultIncrease"),
1526
1563
  monologue: parsed.monologue || "",
1527
1564
  });
1528
1565
  }
@@ -1546,13 +1583,9 @@ Right now it's ${temporalState.description}.`;
1546
1583
  url: currentUrl,
1547
1584
  });
1548
1585
  }
1549
- // Live viewer dwell — when a fresh attention map was computed this step,
1550
- // hold briefly before executing the action so the viewer can register the
1551
- // heatmap. 1.2s is enough to glance at it without making the journey feel
1552
- // sluggish. Skipped on non-capture steps so the journey paces normally.
1553
- if (options.enableMouseOverlay && attentionHeatmapBase64) {
1554
- await new Promise((r) => setTimeout(r, 1200));
1555
- }
1586
+ // (A 1.2 s live-viewer dwell after a step with a fresh heatmap stood
1587
+ // here until 21.0. Nothing assigns attentionHeatmapBase64 since the
1588
+ // heatmap moved into the page, so the pause could never run.)
1556
1589
  // Verbose output
1557
1590
  if (options.verbose) {
1558
1591
  console.log(`\n━━━ Step ${step} ━━━`);
@@ -1570,11 +1603,12 @@ Right now it's ${temporalState.description}.`;
1570
1603
  // v18.29.0: Validate goal evidence against actual page content
1571
1604
  if (goalEvidence) {
1572
1605
  try {
1573
- const pageText = await page.evaluate(() => document.body.innerText?.substring(0, 5000) || '');
1574
- const evidenceWords = goalEvidence.toLowerCase().split(/\s+/).filter((w) => w.length > 3);
1606
+ const pageText = await page.evaluate((n) => document.body.innerText?.substring(0, n) || '', param("journey.evidence.pageChars"));
1607
+ const minWordChars = param("journey.evidence.minWordChars");
1608
+ const evidenceWords = goalEvidence.toLowerCase().split(/\s+/).filter((w) => w.length > minWordChars);
1575
1609
  const matchCount = evidenceWords.filter((w) => pageText.toLowerCase().includes(w)).length;
1576
1610
  const matchRatio = evidenceWords.length > 0 ? matchCount / evidenceWords.length : 0;
1577
- if (matchRatio < 0.3) {
1611
+ if (matchRatio < param("journey.evidence.verifiedShare")) {
1578
1612
  // Evidence doesn't match page content — flag as unverified
1579
1613
  goalEvidence = `[UNVERIFIED - ${Math.round(matchRatio * 100)}% match] ${goalEvidence}`;
1580
1614
  }
@@ -1594,8 +1628,8 @@ Right now it's ${temporalState.description}.`;
1594
1628
  fullMonologue.push(abandonment.message);
1595
1629
  // v18.29.0: Capture failure context
1596
1630
  try {
1597
- const pageText = await page.evaluate(() => document.body.innerText?.substring(0, 1000) || '');
1598
- failureReason = parsed.failureReason || `Abandoned on "${pageTitle}" (${currentUrl}). Last visible: ${pageText.substring(0, 200)}...`;
1631
+ const pageText = await page.evaluate((n) => document.body.innerText?.substring(0, n) || '', param("journey.quoteChars"));
1632
+ failureReason = parsed.failureReason || `Abandoned on "${pageTitle}" (${currentUrl}). Last visible: ${pageText}...`;
1599
1633
  }
1600
1634
  catch {
1601
1635
  failureReason = parsed.failureReason || `Abandoned on "${pageTitle}" (${currentUrl})`;
@@ -1617,7 +1651,7 @@ Right now it's ${temporalState.description}.`;
1617
1651
  // @ts-expect-error window helper installed by overlay script
1618
1652
  if (window.__cbShowAction)
1619
1653
  window.__cbShowAction(t);
1620
- }, label.slice(0, 120));
1654
+ }, label.slice(0, param("journey.live.bannerChars")));
1621
1655
  }
1622
1656
  catch { /* page navigated mid-call, helper will reinstall */ }
1623
1657
  }
@@ -1629,41 +1663,8 @@ Right now it's ${temporalState.description}.`;
1629
1663
  target = availableElements[parseInt(indexMatch[1], 10)];
1630
1664
  }
1631
1665
  else if (textMatch && !textMatch[1].startsWith("#")) {
1632
- // Find best element match by text. Prefer (1) exact match, then
1633
- // (2) starts-with, then (3) includes; among ties, pick the one
1634
- // closest to the current viewport center.
1635
- const wanted = textMatch[1].trim().toLowerCase();
1636
- const vc = viewportCtx.viewport.height / 2;
1637
- const candidates = availableElements.filter(e => e.text);
1638
- const score = (e) => {
1639
- const t = (e.text || "").toLowerCase().trim();
1640
- if (!t)
1641
- return -Infinity;
1642
- let base = 0;
1643
- if (t === wanted)
1644
- base = 1000;
1645
- else if (t.startsWith(wanted) || wanted.startsWith(t))
1646
- base = 500;
1647
- else if (t.includes(wanted) || wanted.includes(t))
1648
- base = 100;
1649
- else
1650
- return -Infinity;
1651
- // Penalize distance from viewport center (closer = preferred)
1652
- const ey = (e.y ?? 0) + (e.height ?? 0) / 2;
1653
- const dist = Math.abs(ey - vc);
1654
- return base - Math.min(dist, 5000);
1655
- };
1656
- let best;
1657
- let bestScore = -Infinity;
1658
- for (const e of candidates) {
1659
- const s = score(e);
1660
- if (s > bestScore) {
1661
- bestScore = s;
1662
- best = e;
1663
- }
1664
- }
1665
- if (best && bestScore > 0)
1666
- target = best;
1666
+ // Find best element match by text (matchClickable).
1667
+ target = matchClickable(availableElements, textMatch[1], viewportCtx.viewport.height);
1667
1668
  }
1668
1669
  let result;
1669
1670
  const hasBbox = target && typeof target.x === "number" && typeof target.y === "number"
@@ -1673,6 +1674,9 @@ Right now it's ${temporalState.description}.`;
1673
1674
  const urlBefore = page.url();
1674
1675
  const cx = target.x + target.width / 2;
1675
1676
  const cy = target.y + target.height / 2;
1677
+ // Set once the pointing wait below is paid, so the retry by name
1678
+ // after a throw does not pay it again.
1679
+ let pointingPaid = false;
1676
1680
  try {
1677
1681
  // Only scroll if the element is OFF-screen. Scrolling an already-
1678
1682
  // visible element causes pointless jitter that stacks with any
@@ -1685,7 +1689,7 @@ Right now it's ${temporalState.description}.`;
1685
1689
  // "instant" behavior — no animation, no stacking with page handlers
1686
1690
  window.scrollTo({ top: t, behavior: "instant" });
1687
1691
  }, { y: cy });
1688
- await page.waitForTimeout(150);
1692
+ await page.waitForTimeout(param("journey.click.scrollSettleMs"));
1689
1693
  }
1690
1694
  // Re-read post-scroll coords; click at center via raw mouse so
1691
1695
  // Playwright's locator auto-scroll-into-view never engages.
@@ -1694,11 +1698,18 @@ Right now it's ${temporalState.description}.`;
1694
1698
  if (!el)
1695
1699
  return null;
1696
1700
  const r = el.getBoundingClientRect();
1697
- return { x: r.left + r.width / 2, y: r.top + r.height / 2, w: r.width, h: r.height };
1701
+ return { x: r.left + r.width / 2, y: r.top + r.height / 2, w: r.width, h: r.height, left: r.left, top: r.top, scrollY: window.scrollY };
1698
1702
  }, { sel: target.selector });
1699
1703
  const ctr = (fresh && fresh.x > 0 && fresh.y > 0)
1700
1704
  ? { x: fresh.x, y: fresh.y, w: fresh.w, h: fresh.h }
1701
1705
  : { x: cx, y: cy, w: target.width, h: target.height };
1706
+ // The pointer's move to the box it is sent to, as a hover's: Stage
1707
+ // B's law from the centre of the screen, plus the gaze lag.
1708
+ const sentTo = (fresh && fresh.x > 0 && fresh.y > 0)
1709
+ ? boxGeometry({ ...target, x: fresh.left, y: fresh.top, width: fresh.w, height: fresh.h }, viewportCtx.viewport, fresh.scrollY)
1710
+ : boxGeometry(target, viewportCtx.viewport, viewportCtx.scroll.y);
1711
+ await pointingWait(fittsParams, state.gazeMouseLag, options.humanInput, sentTo);
1712
+ pointingPaid = true;
1702
1713
  if (options.humanInput) {
1703
1714
  const { humanClick } = await import("../visual/human-input.js");
1704
1715
  await humanClick(page, ctr.x, ctr.y, inputTraits, { width: ctr.w, height: ctr.h });
@@ -1706,20 +1717,21 @@ Right now it's ${temporalState.description}.`;
1706
1717
  else {
1707
1718
  await page.mouse.click(ctr.x, ctr.y);
1708
1719
  }
1709
- await page.waitForTimeout(800);
1720
+ await page.waitForTimeout(param("journey.action.navigationWaitMs"));
1710
1721
  const urlAfter = page.url();
1711
1722
  result = { success: true, ...(urlAfter !== urlBefore ? { newUrl: urlAfter } : {}) };
1712
1723
  }
1713
1724
  catch (clickErr) {
1714
1725
  console.warn(`[cognitive] coordinate click failed: ${clickErr.message}`);
1715
1726
  const fallback = target.text ? `click:${target.text}` : parsed.action;
1716
- result = await executeAction(browser, fallback, fittsParams, state.gazeMouseLag, options.humanInput, inputTraits);
1727
+ result = await executeAction(browser, fallback, fittsParams, state.gazeMouseLag, options.humanInput, inputTraits, boxGeometry(target, viewportCtx.viewport, viewportCtx.scroll.y), pointingPaid);
1717
1728
  }
1718
1729
  }
1719
1730
  else {
1720
1731
  // No bbox — let executeAction handle (non-click actions, or click
1721
- // text not found in availableElements).
1722
- result = await executeAction(browser, parsed.action, fittsParams, state.gazeMouseLag, options.humanInput, inputTraits);
1732
+ // text not found in availableElements). A hover names its element
1733
+ // the same way; its listed geometry, if any, times the pointing.
1734
+ result = await executeAction(browser, parsed.action, fittsParams, state.gazeMouseLag, options.humanInput, inputTraits, pointingGeometry(parsed.action, availableElements, viewportCtx.viewport, viewportCtx.scroll.y));
1723
1735
  }
1724
1736
  // Inter-action settle pause — humans don't fire actions back-to-back
1725
1737
  // at machine speed. Modulated by patience + readingTendency so an
@@ -1759,8 +1771,9 @@ Right now it's ${temporalState.description}.`;
1759
1771
  // Goal progress cap was previously +0.05 max (no-evidence fallback).
1760
1772
  // If the URL actually changed, expand the cap to the LLM's full
1761
1773
  // proposed delta — that's strong navigation evidence.
1762
- if (urlChanged && _llmProposedProgressDelta > 0.05) {
1763
- const additionalAllowed = _llmProposedProgressDelta - 0.05;
1774
+ const inPageCap = param("journey.progress.inPageCap");
1775
+ if (urlChanged && _llmProposedProgressDelta > inPageCap) {
1776
+ const additionalAllowed = _llmProposedProgressDelta - inPageCap;
1764
1777
  state.goalProgress = Math.min(1, state.goalProgress + additionalAllowed);
1765
1778
  }
1766
1779
  // Action result feeds back into the canonical EmotionalState via triggers.
@@ -1768,10 +1781,10 @@ Right now it's ${temporalState.description}.`;
1768
1781
  // which were overwritten next step by deriveSimplifiedState anyway.
1769
1782
  if (state.emotionalState) {
1770
1783
  if (result.success) {
1771
- const progressMade = urlChanged || state.goalProgress > 0.1;
1784
+ const progressMade = urlChanged || state.goalProgress > param("journey.progress.madeLevel");
1772
1785
  // Progress = stronger positive trigger than plain success
1773
1786
  const { state: newEmotional, event } = applyEmotionalTrigger(state.emotionalState, progressMade ? "progress" : "success", emotionalConfig, state.stepCount, {
1774
- severity: progressMade ? 1.2 : 0.8,
1787
+ severity: progressMade ? param("journey.severity.progress") : param("journey.severity.success"),
1775
1788
  description: progressMade
1776
1789
  ? `Action advanced goal (${urlChanged ? "URL change" : "in-page progress"})`
1777
1790
  : `Action completed: ${parsed.action}`,
@@ -1798,7 +1811,7 @@ Right now it's ${temporalState.description}.`;
1798
1811
  // Action threw — fire an error trigger on canonical state.
1799
1812
  if (state.emotionalState) {
1800
1813
  const { state: newEmotional, event } = applyEmotionalTrigger(state.emotionalState, "error", emotionalConfig, state.stepCount, {
1801
- severity: 1.3,
1814
+ severity: param("journey.severity.actionThrew"),
1802
1815
  description: error instanceof Error ? error.message : String(error),
1803
1816
  });
1804
1817
  state.emotionalState = newEmotional;
@@ -1822,15 +1835,15 @@ Right now it's ${temporalState.description}.`;
1822
1835
  // Estimate decision complexity: clicks on links = easy (2 options),
1823
1836
  // form fills = medium (5), navigation choices = hard (8+)
1824
1837
  const estimatedOptions = parsed.action.toLowerCase().includes("fill")
1825
- ? 5
1838
+ ? param("journey.fatigue.options.fill")
1826
1839
  : parsed.action.toLowerCase().includes("navigate") || parsed.action.toLowerCase().includes("search")
1827
- ? 8
1828
- : 3;
1840
+ ? param("journey.fatigue.options.navigate")
1841
+ : param("journey.fatigue.options.other");
1829
1842
  const fatigueIncrement = calculateFatigueIncrement(estimatedOptions);
1830
1843
  state.decisionFatigue.fatigueLevel = Math.min(1, state.decisionFatigue.fatigueLevel + fatigueIncrement);
1831
1844
  state.decisionFatigue.lastDecisionComplexity = estimatedOptions;
1832
1845
  // High fatigue triggers default-seeking behavior
1833
- if (state.decisionFatigue.fatigueLevel > 0.7) {
1846
+ if (state.decisionFatigue.fatigueLevel > param("journey.fatigue.defaultsLevel")) {
1834
1847
  state.decisionFatigue.choosingDefaults = true;
1835
1848
  }
1836
1849
  if (options.verbose) {
@@ -1839,13 +1852,13 @@ Right now it's ${temporalState.description}.`;
1839
1852
  }
1840
1853
  }
1841
1854
  // Small delay between steps
1842
- await sleep(500);
1855
+ await sleep(param("journey.stepPauseMs"));
1843
1856
  }
1844
1857
  // Post-journey goal validation — final evaluation of the current page
1845
1858
  if (!goalAchieved) {
1846
1859
  try {
1847
1860
  const page = await browser.getPage();
1848
- const finalPageText = await page.evaluate(() => document.body.innerText?.substring(0, 3000) || '');
1861
+ const finalPageText = await page.evaluate((n) => document.body.innerText?.substring(0, n) || '', param("journey.finalEval.pageChars"));
1849
1862
  const finalUrl = page.url();
1850
1863
  // Ask Claude: did we actually achieve the goal? Through the decision
1851
1864
  // provider, recorded as the trace's final step, so a replay answers
@@ -1888,7 +1901,7 @@ Answer with JSON:
1888
1901
  const evalJson = (readReplyJson(evalText).value ?? {});
1889
1902
  if (evalJson.achieved) {
1890
1903
  goalAchieved = true;
1891
- goalEvidence = `[FINAL EVAL] ${evalJson.evidence || finalPageText.substring(0, 200)}`;
1904
+ goalEvidence = `[FINAL EVAL] ${evalJson.evidence || finalPageText.substring(0, param("journey.quoteChars"))}`;
1892
1905
  abandonmentReason = undefined;
1893
1906
  abandonmentMessage = undefined;
1894
1907
  }
@@ -1898,13 +1911,13 @@ Answer with JSON:
1898
1911
  catch { }
1899
1912
  }
1900
1913
  // Fallback: only if goalProgress is very high (90%+) AND final monologue explicitly says "found"
1901
- if (!goalAchieved && state.goalProgress >= 0.9) {
1914
+ if (!goalAchieved && state.goalProgress >= param("journey.fallback.progress")) {
1902
1915
  const lastMonologue = (fullMonologue[fullMonologue.length - 1] || '').toLowerCase();
1903
1916
  const strongSignals = ['found it', 'found exactly', 'this is exactly', 'goal achieved', 'here it is', 'perfect, this is'];
1904
1917
  const hasStrongSignal = strongSignals.some(s => lastMonologue.includes(s));
1905
1918
  if (hasStrongSignal) {
1906
1919
  goalAchieved = true;
1907
- goalEvidence = `[AUTO-DETECTED: goalProgress=${Math.round(state.goalProgress * 100)}%] ${fullMonologue[fullMonologue.length - 1]?.substring(0, 200) || ''}`;
1920
+ goalEvidence = `[AUTO-DETECTED: goalProgress=${Math.round(state.goalProgress * 100)}%] ${fullMonologue[fullMonologue.length - 1]?.substring(0, param("journey.quoteChars")) || ''}`;
1908
1921
  abandonmentReason = undefined;
1909
1922
  abandonmentMessage = undefined;
1910
1923
  }
@@ -1927,7 +1940,7 @@ Answer with JSON:
1927
1940
  ...(traceWriter ? { tracePath: traceWriter.filePath } : {}),
1928
1941
  abandonmentReason,
1929
1942
  abandonmentMessage,
1930
- totalTime: (Date.now() - startTime) / 1000,
1943
+ totalTime: (Date.now() - startTime) / param("journey.msPerSecond"),
1931
1944
  stepCount: state.stepCount,
1932
1945
  frictionPoints,
1933
1946
  fullMonologue,
@@ -1936,7 +1949,7 @@ Answer with JSON:
1936
1949
  avgConfusionLevel: emotion.avgConfusionLevel,
1937
1950
  maxFrustrationLevel: emotion.maxFrustrationLevel,
1938
1951
  backtrackCount: state.memory.backtrackCount,
1939
- timeInConfusion: frictionPoints.length * 2, // Estimate
1952
+ timeInConfusion: frictionPoints.length * param("journey.summary.secondsPerFriction"), // Estimate
1940
1953
  // Decision fatigue metrics (v9.9.0)
1941
1954
  decisionsMade: state.decisionFatigue?.decisionsMade ?? 0,
1942
1955
  finalDecisionFatigue: state.decisionFatigue?.fatigueLevel ?? 0,
@@ -1965,17 +1978,21 @@ Answer with JSON:
1965
1978
  // ============================================================================
1966
1979
  function buildCognitiveSystemPrompt(persona, profile, goal, thresholds) {
1967
1980
  const traits = profile.traits;
1981
+ const lowP = param("journey.band.low.patience"), highP = param("journey.band.high.patience");
1982
+ const lowR = param("journey.band.low.riskTolerance"), highR = param("journey.band.high.riskTolerance");
1983
+ const lowC = param("journey.band.low.comprehension"), highC = param("journey.band.high.comprehension");
1984
+ const lowT = param("journey.band.low.readingTendency"), highT = param("journey.band.high.readingTendency");
1968
1985
  return `You are simulating a "${persona.name}" user navigating a website.
1969
1986
 
1970
1987
  PERSONA DESCRIPTION: ${persona.description}
1971
1988
 
1972
1989
  COGNITIVE TRAITS:
1973
- - Patience: ${traits.patience.toFixed(2)} ${traits.patience < 0.3 ? "(impatient)" : traits.patience > 0.7 ? "(patient)" : "(moderate)"}
1974
- - Risk Tolerance: ${traits.riskTolerance.toFixed(2)} ${traits.riskTolerance < 0.3 ? "(cautious)" : traits.riskTolerance > 0.7 ? "(bold)" : "(moderate)"}
1975
- - Comprehension: ${traits.comprehension.toFixed(2)} ${traits.comprehension < 0.4 ? "(struggles with UI)" : traits.comprehension > 0.7 ? "(expert)" : "(moderate)"}
1990
+ - Patience: ${traits.patience.toFixed(2)} ${traits.patience < lowP ? "(impatient)" : traits.patience > highP ? "(patient)" : "(moderate)"}
1991
+ - Risk Tolerance: ${traits.riskTolerance.toFixed(2)} ${traits.riskTolerance < lowR ? "(cautious)" : traits.riskTolerance > highR ? "(bold)" : "(moderate)"}
1992
+ - Comprehension: ${traits.comprehension.toFixed(2)} ${traits.comprehension < lowC ? "(struggles with UI)" : traits.comprehension > highC ? "(expert)" : "(moderate)"}
1976
1993
  - Persistence: ${traits.persistence.toFixed(2)}
1977
1994
  - Curiosity: ${traits.curiosity.toFixed(2)}
1978
- - Reading Tendency: ${traits.readingTendency.toFixed(2)} ${traits.readingTendency < 0.3 ? "(scans only)" : traits.readingTendency > 0.7 ? "(reads everything)" : "(selective reader)"}
1995
+ - Reading Tendency: ${traits.readingTendency.toFixed(2)} ${traits.readingTendency < lowT ? "(scans only)" : traits.readingTendency > highT ? "(reads everything)" : "(selective reader)"}
1979
1996
 
1980
1997
  ATTENTION PATTERN: ${profile.attentionPattern}
1981
1998
  DECISION STYLE: ${profile.decisionStyle}
@@ -2085,7 +2102,7 @@ function buildStepPrompt(state, currentUrl, pageTitle, step, availableElements =
2085
2102
  let viewportStr = "";
2086
2103
  if (viewportCtx) {
2087
2104
  const { viewport, scroll, headings, activeModals, formErrors, bannersBlocking } = viewportCtx;
2088
- const visHeadings = headings.filter(h => h.visible).slice(0, 6).map(h => `H${h.level}: ${h.text}`).join(' | ');
2105
+ const visHeadings = headings.filter(h => h.visible).slice(0, param("journey.prompt.headings")).map(h => `H${h.level}: ${h.text}`).join(' | ');
2089
2106
  const lines = [
2090
2107
  `- Viewport: ${viewport.width}×${viewport.height}`,
2091
2108
  `- Scroll: ${scroll.y}/${scroll.max}px (${Math.round(scroll.pct * 100)}%)${scroll.atTop ? ' [at top]' : ''}${scroll.atBottom ? ' [at bottom]' : ''}`,
@@ -2113,15 +2130,15 @@ function buildStepPrompt(state, currentUrl, pageTitle, step, availableElements =
2113
2130
  else if (lastOutcome.action.startsWith('click')) {
2114
2131
  lines.push(`- Page did NOT navigate (click had no nav effect — element may be JS-only or wasn't matched)`);
2115
2132
  }
2116
- if (typeof lastOutcome.scrollDelta === 'number' && Math.abs(lastOutcome.scrollDelta) > 10) {
2133
+ if (typeof lastOutcome.scrollDelta === 'number' && Math.abs(lastOutcome.scrollDelta) > param("journey.prompt.scrollReportPx")) {
2117
2134
  const dir = lastOutcome.scrollDelta > 0 ? 'down' : 'up';
2118
2135
  lines.push(`- Scrolled ${dir} ${Math.abs(lastOutcome.scrollDelta)}px`);
2119
2136
  }
2120
2137
  if (lastOutcome.newClickables?.length) {
2121
- lines.push(`- New elements appeared: ${lastOutcome.newClickables.slice(0, 5).map(t => `"${t}"`).join(', ')}`);
2138
+ lines.push(`- New elements appeared: ${lastOutcome.newClickables.slice(0, param("journey.prompt.changedElementsShown")).map(t => `"${t}"`).join(', ')}`);
2122
2139
  }
2123
2140
  if (lastOutcome.removedClickables?.length) {
2124
- lines.push(`- Elements no longer visible: ${lastOutcome.removedClickables.slice(0, 5).map(t => `"${t}"`).join(', ')}`);
2141
+ lines.push(`- Elements no longer visible: ${lastOutcome.removedClickables.slice(0, param("journey.prompt.changedElementsShown")).map(t => `"${t}"`).join(', ')}`);
2125
2142
  }
2126
2143
  outcomeStr = `\nWHAT HAPPENED LAST STEP:\n${lines.join('\n')}\n`;
2127
2144
  }
@@ -2148,14 +2165,16 @@ CURRENT STATE:
2148
2165
  ${(() => {
2149
2166
  // Loop detection: if the last 4 actions are alternating scrolls, surface
2150
2167
  // it loudly so the AI breaks out instead of pumping scroll:down/up forever.
2151
- const recent = state.memory.actionsAttempted.slice(-4).map(a => a.action || "");
2152
- if (recent.length >= 4 && recent.every(a => a.startsWith("scroll:"))) {
2153
- return `\n⚠️ LOOP DETECTED: your last 4 actions were all scrolls (${recent.join(", ")}). Pick a click/back/dismiss/find action instead — scrolling more will not help.`;
2168
+ const scrollLoop = param("journey.prompt.scrollLoop");
2169
+ const recent = state.memory.actionsAttempted.slice(-scrollLoop).map(a => a.action || "");
2170
+ if (recent.length >= scrollLoop && recent.every(a => a.startsWith("scroll:"))) {
2171
+ return `\n⚠️ LOOP DETECTED: your last ${scrollLoop} actions were all scrolls (${recent.join(", ")}). Pick a click/back/dismiss/find action instead — scrolling more will not help.`;
2154
2172
  }
2155
2173
  // Repeated failed actions
2156
- const lastTwo = state.memory.actionsAttempted.slice(-2);
2157
- if (lastTwo.length === 2 && lastTwo.every(a => a.success === false)) {
2158
- return `\n⚠️ Last 2 actions failed (${lastTwo.map(a => a.action).join(", ")}). The element you wanted may not exist with that exact text — try a click:#N index from AVAILABLE ELEMENTS above, or a different element entirely.`;
2174
+ const failedRun = param("journey.prompt.failedRun");
2175
+ const lastTwo = state.memory.actionsAttempted.slice(-failedRun);
2176
+ if (lastTwo.length === failedRun && lastTwo.every(a => a.success === false)) {
2177
+ return `\n⚠️ Last ${failedRun} actions failed (${lastTwo.map(a => a.action).join(", ")}). The element you wanted may not exist with that exact text — try a click:#N index from AVAILABLE ELEMENTS above, or a different element entirely.`;
2159
2178
  }
2160
2179
  return "";
2161
2180
  })()}
@@ -2232,37 +2251,7 @@ export function resolveJourneyPersona(name, customTraits) {
2232
2251
  else if (customTraits) {
2233
2252
  // v16.7.1: full default trait set (was only 7, causing trait dropout).
2234
2253
  // Said 25 until 2026-08-01; the model has 26.
2235
- const defaultTraits = {
2236
- // Required traits (7)
2237
- patience: 0.5,
2238
- riskTolerance: 0.5,
2239
- comprehension: 0.5,
2240
- persistence: 0.5,
2241
- curiosity: 0.5,
2242
- workingMemory: 0.5,
2243
- readingTendency: 0.5,
2244
- // Optional traits - Tier 1: Core
2245
- resilience: 0.5,
2246
- selfEfficacy: 0.5,
2247
- satisficing: 0.5,
2248
- trustCalibration: 0.5,
2249
- interruptRecovery: 0.5,
2250
- // Optional traits - Tier 2+
2251
- informationForaging: 0.5,
2252
- changeBlindness: 0.3,
2253
- anchoringBias: 0.5,
2254
- timeHorizon: 0.5,
2255
- attributionStyle: 0.5,
2256
- metacognitivePlanning: 0.5,
2257
- proceduralFluency: 0.5,
2258
- transferLearning: 0.5,
2259
- authoritySensitivity: 0.5,
2260
- emotionalContagion: 0.5,
2261
- fearOfMissingOut: 0.5,
2262
- socialProofSensitivity: 0.5,
2263
- mentalModelFlexibility: 0.5,
2264
- siteFamiliarity: 0.5,
2265
- };
2254
+ const defaultTraits = defaultJourneyTraits();
2266
2255
  personaObj = {
2267
2256
  ...existingPersona,
2268
2257
  cognitiveTraits: {
@@ -2291,7 +2280,7 @@ export function createLiveDecisionProvider(anthropic, model) {
2291
2280
  decide: async (ctx) => {
2292
2281
  if (!anthropic)
2293
2282
  throw new Error("Anthropic API key not configured.");
2294
- const ask = (messages, maxTokens = 2000) => anthropic.messages.create({
2283
+ const ask = (messages, maxTokens = param("journey.llm.maxTokens")) => anthropic.messages.create({
2295
2284
  model,
2296
2285
  max_tokens: maxTokens,
2297
2286
  ...(ctx.systemPrompt ? { system: ctx.systemPrompt } : {}),
@@ -2315,7 +2304,7 @@ export function createLiveDecisionProvider(anthropic, model) {
2315
2304
  ...ctx.messages,
2316
2305
  { role: "assistant", content: firstText.trim() || "(no text)" },
2317
2306
  { role: "user", content: "Your reply could not be read as the JSON object the response format asks for. Reply again with only that JSON object, nothing before or after it." },
2318
- ], first.stop_reason === "max_tokens" ? 8000 : 2000);
2307
+ ], first.stop_reason === "max_tokens" ? param("journey.llm.retryMaxTokens") : param("journey.llm.maxTokens"));
2319
2308
  const retryText = textOf(retry);
2320
2309
  if (readReplyJson(retryText).value)
2321
2310
  return retryText;
@@ -2418,19 +2407,12 @@ function checkAbandonmentTriggers(state, thresholds) {
2418
2407
  message: "Too many choices... I can't think straight anymore. Maybe later.",
2419
2408
  };
2420
2409
  }
2421
- // Loop detection
2422
- const recentPages = state.memory.pagesVisited.slice(-5);
2423
- const uniqueRecent = new Set(recentPages).size;
2424
- if (recentPages.length >= 5 &&
2425
- uniqueRecent <= thresholds.loopDetectionThreshold) {
2426
- return {
2427
- reason: "loop",
2428
- message: "I keep ending up on the same pages. Something is wrong.",
2429
- };
2430
- }
2410
+ // (Loop detection stood here until 21.0: abandon when the last 5 pages
2411
+ // held 3 or fewer distinct ones. pagesVisited records each page once, so
2412
+ // the last 5 were always 5 distinct pages and it could never fire.)
2431
2413
  // No progress
2432
2414
  if (state.stepCount > thresholds.maxStepsWithoutProgress &&
2433
- state.goalProgress < 0.1) {
2415
+ state.goalProgress < param("journey.progress.madeLevel")) {
2434
2416
  return {
2435
2417
  reason: "no_progress",
2436
2418
  message: "I'm not making any progress. This isn't working.",
@@ -2440,30 +2422,21 @@ function checkAbandonmentTriggers(state, thresholds) {
2440
2422
  if (state.emotionalState) {
2441
2423
  const emotional = state.emotionalState;
2442
2424
  // High anxiety + high frustration — only abandon if BOTH are extreme AND patience is depleted
2443
- if (emotional.anxiety > 0.85 && emotional.frustration > 0.8 && state.patienceRemaining < 0.3) {
2425
+ if (emotional.anxiety > param("journey.emotionalAbandon.anxiety") && emotional.frustration > param("journey.emotionalAbandon.frustration") && state.patienceRemaining < param("journey.emotionalAbandon.patience")) {
2444
2426
  return {
2445
2427
  reason: "emotional",
2446
2428
  message: "This is too stressful. I need to take a break.",
2447
2429
  };
2448
2430
  }
2449
- // Extreme boredom — only after significant time with zero engagement
2450
- if (emotional.boredom > 0.9 && emotional.excitement < 0.1 && state.stepCount > 8) {
2451
- return {
2452
- reason: "emotional",
2453
- message: "This is so boring... I'll do this later.",
2454
- };
2455
- }
2456
- // Overwhelming negative valence — only with sustained high arousal AND low patience
2457
- if (emotional.valence < -0.85 && emotional.arousal > 0.85 && state.patienceRemaining < 0.4) {
2458
- return {
2459
- reason: "emotional",
2460
- message: "I can't deal with this right now.",
2461
- };
2462
- }
2431
+ // Two rules stood here until 21.0 that no journey can reach: extreme
2432
+ // boredom (boredom > 0.9), when no trigger the journey fires raises
2433
+ // boredom above its starting value of at most 0.1; and very negative
2434
+ // valence (< -0.85), when valence is an average of the emotions'
2435
+ // valences and the most negative of those is -0.8.
2463
2436
  // Apply emotional abandonment modifier to patience threshold
2464
2437
  const emotionalModifier = calculateAbandonmentModifier(emotional);
2465
2438
  const adjustedPatienceThreshold = thresholds.patienceMin * emotionalModifier;
2466
- if (state.patienceRemaining < adjustedPatienceThreshold && emotionalModifier > 1.3) {
2439
+ if (state.patienceRemaining < adjustedPatienceThreshold && emotionalModifier > param("journey.emotionalAbandon.modifier")) {
2467
2440
  return {
2468
2441
  reason: "emotional",
2469
2442
  message: "I don't have the energy for this anymore.",
@@ -2495,27 +2468,119 @@ function ordinalSuffix(n) {
2495
2468
  default: return "th";
2496
2469
  }
2497
2470
  }
2498
- async function executeAction(browser, action, fittsParams, gazeMouseLag, humanInput, inputTraits) {
2471
+ /**
2472
+ * The listed clickable a text names. Prefer (1) an exact match, then (2)
2473
+ * starts-with, then (3) includes, each scored less its distance from the
2474
+ * viewport's middle (journey.clickMatch.*); none unless the best score is
2475
+ * positive, so a match further away than its own score is never chosen.
2476
+ */
2477
+ function matchClickable(elements, text, viewportHeight) {
2478
+ const wanted = text.trim().toLowerCase();
2479
+ const vc = viewportHeight / 2;
2480
+ const candidates = elements.filter(e => e.text);
2481
+ const score = (e) => {
2482
+ const t = (e.text || "").toLowerCase().trim();
2483
+ if (!t)
2484
+ return -Infinity;
2485
+ let base = 0;
2486
+ if (t === wanted)
2487
+ base = param("journey.clickMatch.exact");
2488
+ else if (t.startsWith(wanted) || wanted.startsWith(t))
2489
+ base = param("journey.clickMatch.prefix");
2490
+ else if (t.includes(wanted) || wanted.includes(t))
2491
+ base = param("journey.clickMatch.contains");
2492
+ else
2493
+ return -Infinity;
2494
+ const ey = (e.y ?? 0) + (e.height ?? 0) / 2;
2495
+ const dist = Math.abs(ey - vc);
2496
+ return base - dist;
2497
+ };
2498
+ let best;
2499
+ let bestScore = -Infinity;
2500
+ for (const e of candidates) {
2501
+ const s = score(e);
2502
+ if (s > bestScore) {
2503
+ bestScore = s;
2504
+ best = e;
2505
+ }
2506
+ }
2507
+ return best && bestScore > 0 ? best : undefined;
2508
+ }
2509
+ /**
2510
+ * The Fitts geometry of a listed element, as Stage B measures pointing: the
2511
+ * distance from the centre of the screen it is seen on (the pointer's assumed
2512
+ * start) to the element's centre, and the smaller of its width and height.
2513
+ * The box is in viewport coordinates, taken with the page scrolled to
2514
+ * `scrollY`. An element whose centre is on the current screen is seen there;
2515
+ * one above or below is seen once scrolled to, on the screen-sized band of
2516
+ * the page that holds its centre (Stage B's full-page tiling). Undefined when
2517
+ * the element has no usable box.
2518
+ */
2519
+ function boxGeometry(el, viewport, scrollY) {
2520
+ if (!el || typeof el.x !== "number" || typeof el.y !== "number" || typeof el.width !== "number" || typeof el.height !== "number")
2521
+ return undefined;
2522
+ if (!(viewport.width > 0 && viewport.height > 0 && el.width >= 0 && el.height >= 0 && Number.isFinite(scrollY)))
2523
+ return undefined;
2524
+ const centreY = el.y + el.height / 2;
2525
+ const onScreen = centreY >= 0 && centreY <= viewport.height;
2526
+ const box = { x: el.x, y: el.y + scrollY, width: el.width, height: el.height };
2527
+ return {
2528
+ distance: distanceToScreenCentre(box, { viewportWidth: viewport.width, viewportHeight: viewport.height, scrollY }, onScreen ? "viewport" : "full_page"),
2529
+ width: smallerOfWidth(box.width, box.height),
2530
+ };
2531
+ }
2532
+ /**
2533
+ * The Fitts geometry of the element a click, hover or hover-click names: by
2534
+ * index (#N), by selector, or by text (matchClickable). Undefined for an
2535
+ * action that moves no pointer, or one whose element is not listed; executeAction
2536
+ * then charges the registered fallback move (journey.fitts.unlocated*).
2537
+ */
2538
+ function pointingGeometry(action, elements, viewport, scrollY) {
2539
+ const [type, ...args] = action.split(":");
2540
+ if (!["click", "hover", "hoverclick"].includes(type))
2541
+ return undefined;
2542
+ const name = type === "hoverclick" ? (args[0] ?? "") : args.join(":");
2543
+ const index = /^#(\d+)$/.exec(name);
2544
+ const el = index
2545
+ ? elements[parseInt(index[1], 10)]
2546
+ : elements.find(e => e.selector === name) ?? matchClickable(elements, name, viewport.height);
2547
+ return boxGeometry(el, viewport, scrollY);
2548
+ }
2549
+ /**
2550
+ * The wait before a mouse action reaches its target: Fitts' law (v9.9.0,
2551
+ * Stage B's law) on the target's measured geometry, or the registered
2552
+ * fallback move for one the journey has not found, plus the gaze-to-mouse
2553
+ * lag. Returns the time, and sleeps it unless humanClick will drive the
2554
+ * pointer (`pacedByHumanClick`).
2555
+ */
2556
+ async function pointingWait(fittsParams, gazeMouseLag, pacedByHumanClick, pointing) {
2557
+ const distance = pointing ? pointing.distance : param("journey.fitts.unlocatedDistancePx");
2558
+ const width = pointing ? pointing.width : param("journey.fitts.unlocatedTargetWidthPx");
2559
+ let movementTimeMs = calculateFittsMovementTime(distance, width, fittsParams);
2560
+ // Add gaze-to-mouse lag (v10.1.0) - WebGazer.js research
2561
+ // Eye movements precede mouse movements by 200-500ms
2562
+ if (gazeMouseLag) {
2563
+ movementTimeMs += gazeMouseLag;
2564
+ }
2565
+ // With human input on, the coordinate click hands the pointer to
2566
+ // humanClick, whose move and pre-click dwell replace this wait, so the two
2567
+ // don't compound (which would make live mode slower than non-live). A
2568
+ // hover, hover-click or click by name goes through Playwright, which has no
2569
+ // human pacing, so it waits this time with human input on or off.
2570
+ if (!pacedByHumanClick) {
2571
+ await sleep(movementTimeMs);
2572
+ }
2573
+ return movementTimeMs;
2574
+ }
2575
+ async function executeAction(browser, action, fittsParams, gazeMouseLag, humanInput, inputTraits, pointing, pointingPaid = false) {
2499
2576
  const [type, ...args] = action.split(":");
2500
- // Apply Fitts' Law timing for mouse actions (v9.9.0)
2577
+ // Mouse actions wait their pointing time (pointingWait), unless the caller
2578
+ // already paid it: the retry by name after a coordinate click throws.
2579
+ // Playwright drives the pointer here, never humanClick, so the wait is
2580
+ // slept with human input on too.
2501
2581
  let movementTimeMs = 0;
2502
- if (["click", "hover", "hoverclick"].includes(type) && fittsParams) {
2503
- // Estimate target size and distance
2504
- // Average button width ~80px, average screen movement distance ~300px
2505
- const estimatedDistance = 300;
2506
- const estimatedTargetWidth = 80;
2507
- movementTimeMs = calculateFittsMovementTime(estimatedDistance, estimatedTargetWidth, fittsParams);
2508
- // Add gaze-to-mouse lag (v10.1.0) - WebGazer.js research
2509
- // Eye movements precede mouse movements by 200-500ms
2510
- if (gazeMouseLag) {
2511
- movementTimeMs += gazeMouseLag;
2512
- }
2513
- // When humanInput is enabled, humanClick provides its own hover-and-dwell
2514
- // pacing — skip the pre-action sleep here so they don't compound (which
2515
- // would make live mode slower than non-live, defeating the purpose).
2516
- if (!humanInput) {
2517
- await sleep(movementTimeMs);
2518
- }
2582
+ if (["click", "hover", "hoverclick"].includes(type) && fittsParams && !pointingPaid) {
2583
+ movementTimeMs = await pointingWait(fittsParams, gazeMouseLag, false, pointing);
2519
2584
  }
2520
2585
  switch (type) {
2521
2586
  case "click": {
@@ -2527,8 +2592,8 @@ async function executeAction(browser, action, fittsParams, gazeMouseLag, humanIn
2527
2592
  try {
2528
2593
  const escapedSel = selector.replace(/"/g, '\\"');
2529
2594
  const link = page.locator(`a:has-text("${escapedSel}")`).first();
2530
- if (await link.isVisible({ timeout: 1500 }).catch(() => false)) {
2531
- await link.click({ timeout: 3000 });
2595
+ if (await link.isVisible({ timeout: param("journey.action.linkVisibleMs") }).catch(() => false)) {
2596
+ await link.click({ timeout: param("journey.action.clickTimeoutMs") });
2532
2597
  clicked = true;
2533
2598
  }
2534
2599
  }
@@ -2537,8 +2602,8 @@ async function executeAction(browser, action, fittsParams, gazeMouseLag, humanIn
2537
2602
  try {
2538
2603
  const escapedSel = selector.replace(/"/g, '\\"');
2539
2604
  const btn = page.locator(`button:has-text("${escapedSel}"), [role="button"]:has-text("${escapedSel}")`).first();
2540
- if (await btn.isVisible({ timeout: 1000 }).catch(() => false)) {
2541
- await btn.click({ timeout: 3000 });
2605
+ if (await btn.isVisible({ timeout: param("journey.action.buttonVisibleMs") }).catch(() => false)) {
2606
+ await btn.click({ timeout: param("journey.action.clickTimeoutMs") });
2542
2607
  clicked = true;
2543
2608
  }
2544
2609
  }
@@ -2550,7 +2615,7 @@ async function executeAction(browser, action, fittsParams, gazeMouseLag, humanIn
2550
2615
  return { success: false, movementTimeMs };
2551
2616
  }
2552
2617
  try {
2553
- await page.waitForTimeout(1000);
2618
+ await page.waitForTimeout(param("journey.action.clickSettleMs"));
2554
2619
  }
2555
2620
  catch { }
2556
2621
  let urlAfter = page.url();
@@ -2561,12 +2626,12 @@ async function executeAction(browser, action, fittsParams, gazeMouseLag, humanIn
2561
2626
  const escapedSel = selector.replace(/"/g, '\\"');
2562
2627
  const href = await page.locator(`a:has-text("${escapedSel}")`).first().getAttribute('href').catch(() => null);
2563
2628
  if (href && href.startsWith('http')) {
2564
- await page.goto(href, { waitUntil: 'domcontentloaded', timeout: 10000 });
2629
+ await page.goto(href, { waitUntil: 'domcontentloaded', timeout: param("journey.action.gotoTimeoutMs") });
2565
2630
  urlAfter = page.url();
2566
2631
  }
2567
2632
  else if (href && href.startsWith('/')) {
2568
2633
  const base = new URL(urlBefore).origin;
2569
- await page.goto(base + href, { waitUntil: 'domcontentloaded', timeout: 10000 });
2634
+ await page.goto(base + href, { waitUntil: 'domcontentloaded', timeout: param("journey.action.gotoTimeoutMs") });
2570
2635
  urlAfter = page.url();
2571
2636
  }
2572
2637
  }
@@ -2601,8 +2666,8 @@ async function executeAction(browser, action, fittsParams, gazeMouseLag, humanIn
2601
2666
  const r = await browser.fill(selector, value);
2602
2667
  return { success: r.success };
2603
2668
  }
2604
- await loc.scrollIntoViewIfNeeded({ timeout: 2000 }).catch(() => { });
2605
- await loc.click({ timeout: 3000 });
2669
+ await loc.scrollIntoViewIfNeeded({ timeout: param("journey.action.focusScrollMs") }).catch(() => { });
2670
+ await loc.click({ timeout: param("journey.action.clickTimeoutMs") });
2606
2671
  // Clear if there's existing content
2607
2672
  await page.keyboard.press("Control+A").catch(() => { });
2608
2673
  await page.keyboard.press("Delete").catch(() => { });
@@ -2620,7 +2685,7 @@ async function executeAction(browser, action, fittsParams, gazeMouseLag, humanIn
2620
2685
  // Use age modifier as proxy for typing expertise (younger = faster)
2621
2686
  const expertise = fittsParams?.ageModifier
2622
2687
  ? Math.max(0, 1 - (fittsParams.ageModifier - 1))
2623
- : 0.5;
2688
+ : param("journey.typing.defaultExpertise");
2624
2689
  const typingTimeMs = calculateTypingTime(value, expertise, true);
2625
2690
  // Add realistic typing delay
2626
2691
  await sleep(typingTimeMs);
@@ -2639,10 +2704,11 @@ async function executeAction(browser, action, fittsParams, gazeMouseLag, humanIn
2639
2704
  const page = await browser.getPage();
2640
2705
  try {
2641
2706
  const beforeY = await page.evaluate(() => window.scrollY);
2707
+ const step = param("journey.action.scrollPx");
2642
2708
  if (humanInput && (direction === "down" || direction === "up")) {
2643
2709
  // Human-realistic scroll: variable delta + occasional glance-back.
2644
2710
  const { humanScroll } = await import("../visual/human-input.js");
2645
- const baseDelta = direction === "down" ? 400 : -400;
2711
+ const baseDelta = direction === "down" ? step : -step;
2646
2712
  await humanScroll(page, baseDelta, inputTraits);
2647
2713
  }
2648
2714
  else {
@@ -2654,20 +2720,20 @@ async function executeAction(browser, action, fittsParams, gazeMouseLag, humanIn
2654
2720
  await page.evaluate(() => window.scrollTo({ top: document.body.scrollHeight, behavior: "smooth" }));
2655
2721
  break;
2656
2722
  case "up":
2657
- await page.evaluate(() => window.scrollBy({ top: -400, behavior: "smooth" }));
2723
+ await page.evaluate((d) => window.scrollBy({ top: -d, behavior: "smooth" }), step);
2658
2724
  break;
2659
2725
  case "down":
2660
2726
  default:
2661
- await page.evaluate(() => window.scrollBy({ top: 400, behavior: "smooth" }));
2727
+ await page.evaluate((d) => window.scrollBy({ top: d, behavior: "smooth" }), step);
2662
2728
  break;
2663
2729
  }
2664
- await sleep(300);
2730
+ await sleep(param("journey.action.scrollSettleMs"));
2665
2731
  }
2666
2732
  // Report success only if the page actually moved. A no-op scroll
2667
2733
  // (already at top/bottom, or scroll blocked) returns failure so the
2668
2734
  // AI knows to try something else instead of looping scroll:up/down.
2669
2735
  const afterY = await page.evaluate(() => window.scrollY);
2670
- const moved = Math.abs(afterY - beforeY) > 5;
2736
+ const moved = Math.abs(afterY - beforeY) > param("journey.action.scrollMovedPx");
2671
2737
  return { success: moved };
2672
2738
  }
2673
2739
  catch {
@@ -2678,8 +2744,8 @@ async function executeAction(browser, action, fittsParams, gazeMouseLag, humanIn
2678
2744
  // Browser back button — most common recovery action
2679
2745
  const page = await browser.getPage();
2680
2746
  try {
2681
- await page.goBack({ waitUntil: "domcontentloaded", timeout: 10000 });
2682
- await sleep(500);
2747
+ await page.goBack({ waitUntil: "domcontentloaded", timeout: param("journey.action.gotoTimeoutMs") });
2748
+ await sleep(param("journey.action.backSettleMs"));
2683
2749
  return { success: true, newUrl: page.url() };
2684
2750
  }
2685
2751
  catch {
@@ -2706,7 +2772,7 @@ async function executeAction(browser, action, fittsParams, gazeMouseLag, humanIn
2706
2772
  }
2707
2773
  return false;
2708
2774
  }, query);
2709
- await sleep(500);
2775
+ await sleep(param("journey.action.findSettleMs"));
2710
2776
  return { success: found };
2711
2777
  }
2712
2778
  catch {
@@ -2718,9 +2784,9 @@ async function executeAction(browser, action, fittsParams, gazeMouseLag, humanIn
2718
2784
  const page = await browser.getPage();
2719
2785
  const count = parseInt(args[0] || "1", 10);
2720
2786
  try {
2721
- for (let i = 0; i < Math.min(count, 10); i++) {
2787
+ for (let i = 0; i < Math.min(count, param("journey.action.maxTabs")); i++) {
2722
2788
  await page.keyboard.press("Tab");
2723
- await sleep(150);
2789
+ await sleep(param("journey.action.tabIntervalMs"));
2724
2790
  }
2725
2791
  return { success: true };
2726
2792
  }
@@ -2733,7 +2799,7 @@ async function executeAction(browser, action, fittsParams, gazeMouseLag, humanIn
2733
2799
  const page = await browser.getPage();
2734
2800
  try {
2735
2801
  await page.keyboard.press("Enter");
2736
- await sleep(800);
2802
+ await sleep(param("journey.action.navigationWaitMs"));
2737
2803
  const newUrl = page.url();
2738
2804
  return { success: true, newUrl };
2739
2805
  }
@@ -2757,7 +2823,7 @@ async function executeAction(browser, action, fittsParams, gazeMouseLag, humanIn
2757
2823
  case "dismiss": {
2758
2824
  // Dismiss overlay/popup/modal — close cookie banners, newsletter popups
2759
2825
  try {
2760
- await browser.dismissOverlay({ type: "auto", timeout: 3000 });
2826
+ await browser.dismissOverlay({ type: "auto", timeout: param("journey.action.dismissTimeoutMs") });
2761
2827
  return { success: true };
2762
2828
  }
2763
2829
  catch {
@@ -2770,7 +2836,7 @@ async function executeAction(browser, action, fittsParams, gazeMouseLag, humanIn
2770
2836
  const page = await browser.getPage();
2771
2837
  try {
2772
2838
  await page.click(`details:has(summary:has-text("${selector}")) summary, [role="tab"]:has-text("${selector}"), button:has-text("${selector}")`);
2773
- await sleep(400);
2839
+ await sleep(param("journey.action.expandSettleMs"));
2774
2840
  return { success: true };
2775
2841
  }
2776
2842
  catch {
@@ -2781,7 +2847,7 @@ async function executeAction(browser, action, fittsParams, gazeMouseLag, humanIn
2781
2847
  }
2782
2848
  case "wait": {
2783
2849
  // Wait for page to load / content to appear
2784
- await sleep(parseInt(args[0] || "1000", 10));
2850
+ await sleep(args[0] ? parseInt(args[0], 10) : param("journey.action.waitDefaultMs"));
2785
2851
  return { success: true };
2786
2852
  }
2787
2853
  default: