diff --git a/bench/tasks/booking.com/booking-house-rules.json b/bench/tasks/booking.com/booking-house-rules.json
index bc5083d5..ac1ed653 100644
--- a/bench/tasks/booking.com/booking-house-rules.json
+++ b/bench/tasks/booking.com/booking-house-rules.json
@@ -1,7 +1,7 @@
{
"id": "booking-house-rules",
"domain": "booking.com",
- "stage": "gap",
+ "stage": "inspect",
"prompt": "On booking.com, find whether {{hotel}} in {{city}} allows pets and whether it accepts cash, as its house rules say. Reply with JSON: {\"pets_allowed\": boolean, \"cash_accepted\": boolean}.",
"fixture": {
"hotel": "Palacio Ramalhete",
diff --git a/domains/booking.com/README.md b/domains/booking.com/README.md
index 5d9695b2..6abcc477 100644
--- a/domains/booking.com/README.md
+++ b/domains/booking.com/README.md
@@ -5,6 +5,6 @@
| continue_checkout | Presses "Next: Final details" on the checkout's details step after checking the total, required fields and that the selection holds the party; never books or pays. |
| fill_guest_details | Types the main guest's name, email, phone and address into the checkout's details step; presses nothing. |
| get_checkout_step | Reads Booking.com's checkout page, including a Partner Offer's partner checkout: the step, property, dates, selected rooms, any warning that they cannot hold the party and the total including taxes and fees. |
-| get_property | Reads one property's dated room rates that hold the party's adults, with IDs, adult and child places, whether they fit every child, cancellation, prepayment, meals and excluded fees, plus its address, reviews and check-in and check-out times. |
+| get_property | Reads one property's dated room rates that hold the party's adults, with IDs, adult and child places, whether they fit every child, cancellation, prepayment, meals and excluded fees, plus its address, reviews, check-in and check-out times and house rules (pets, payment methods and cash, children and beds). |
| search_stays | Searches stays for a destination, dates and party in the site's current currency and returns the first results page with prices and reviews. |
| select_room | Selects one room of an exact rate, including a Partner Offer, on the open property page for the expected party and presses "I'll reserve", which opens the details step without booking. |
diff --git a/domains/booking.com/get_property.js b/domains/booking.com/get_property.js
index 8d2f0d8f..16120180 100644
--- a/domains/booking.com/get_property.js
+++ b/domains/booking.com/get_property.js
@@ -29,7 +29,7 @@
}
};
const propertySchema = {
- type: "object", additionalProperties: false, required: ["property_id", "name", "property_url", "page_url", "address", "review", "review_categories", "popular_facilities", "check_in_time", "check_out_time"],
+ type: "object", additionalProperties: false, required: ["property_id", "name", "property_url", "page_url", "address", "review", "review_categories", "popular_facilities", "check_in_time", "check_out_time", "house_rules", "house_rules_truncated"],
properties: {
property_id: { type: ["string", "null"], description: "Booking.com's numeric property id." },
name: { type: "string" },
@@ -44,7 +44,16 @@
review_categories: { type: "array", items: { type: "object", additionalProperties: false, required: ["category", "score"], properties: { category: { type: "string" }, score: { type: "number" } } } },
popular_facilities: { type: "array", items: { type: "string" } },
check_in_time: { type: ["string", "null"], description: "The house-rules text, for example 'From 2:00 PM'." },
- check_out_time: { type: ["string", "null"] }
+ check_out_time: { type: ["string", "null"] },
+ house_rules: {
+ type: ["array", "null"], maxItems: 20,
+ items: {
+ type: "object", additionalProperties: false, required: ["title", "lines"],
+ properties: { title: { type: "string" }, lines: { type: "array", maxItems: 30, items: { type: "string" } } }
+ },
+ description: "Every house-rules section in page order, with its title as the page shows it (for example 'Pets', 'Accepted payment methods' or 'Cards accepted at this property', 'Children & Beds', 'Age restriction', 'Smoking', 'Parties', 'Quiet hours') and its text lines: card logos by name, and each crib or extra-bed row as one line ('4 - 12 years: Extra bed upon request, € 12.50 per child, per night', with '; ' between offers for the same ages). A section the property did not fill in is absent, not 'allowed'. Empty when the page has no house rules; null when it has a house-rules section the tool could not read, which says nothing about any rule."
+ },
+ house_rules_truncated: { type: "boolean", description: "True when house_rules leaves out sections or lines the page lists (more than 20 sections, more than 30 lines in a section, or about 20 KB of text); read the rest on the property page." }
}
};
return [{
@@ -52,7 +61,7 @@
match: { url_patterns: ["https://www.booking.com/*"] },
tool: {
name: "get_property",
- description: "Read one Booking.com property page for given dates and party: name, address, review score, popular facilities, check-in and check-out times, and every room with each rate the site lists for the requested party (displayed price for the whole stay, cancellation, prepayment and meal terms, taxes and fees excluded from the price). Get property_url from search_stays. Requires explicit check_in, check_out, adults and currency, for one room. currency must be the currency the site is already showing (its header currency picker): the tool never changes the site's currency and fails with currency_mismatch, naming what the site shows, when they differ. The output echoes the effective dates, guests and currency the page applied and a price_basis, and a page that did not apply them as asked is an error. Rates the page limits to fewer adults than the party are left out, because their prices do not cover the whole party (rates_left_out counts them); when every rate is like that the status is no_rate_for_party. With children, a rate with places for the adults but not for every child is kept, marked fits_party false and ranked after the rates that fit, before max_rates applies. A rate marked as a members-only price needs signing in: price.total then includes a member discount (member_discount) that a signed-out visitor is not given. Read-only: it never selects a room or rate, saves to a list, reserves or opens checkout, and changes no browser or account setting; Booking.com records the page view. Listing a rate is not a hold or a guarantee, and prices can differ by session (member discounts when signed in). Run it on any https://www.booking.com page in an English-language session and do not navigate the tab while it runs. Dates the property cannot offer give status no_availability, and so can dates beyond the site's bookable window; for such dates the page may also apply the party the session last searched, which is reported as search_not_applied.",
+ description: "Read one Booking.com property page for given dates and party: name, address, review score, popular facilities, check-in and check-out times, the house rules (pets, accepted payment methods and cash, children and extra beds, age restriction, smoking, parties, quiet hours, as the property lists them), and every room with each rate the site lists for the requested party (displayed price for the whole stay, cancellation, prepayment and meal terms, taxes and fees excluded from the price). Get property_url from search_stays. Requires explicit check_in, check_out, adults and currency, for one room. currency must be the currency the site is already showing (its header currency picker): the tool never changes the site's currency and fails with currency_mismatch, naming what the site shows, when they differ. The output echoes the effective dates, guests and currency the page applied and a price_basis, and a page that did not apply them as asked is an error. Rates the page limits to fewer adults than the party are left out, because their prices do not cover the whole party (rates_left_out counts them); when every rate is like that the status is no_rate_for_party. With children, a rate with places for the adults but not for every child is kept, marked fits_party false and ranked after the rates that fit, before max_rates applies. A rate marked as a members-only price needs signing in: price.total then includes a member discount (member_discount) that a signed-out visitor is not given. Read-only: it never selects a room or rate, saves to a list, reserves or opens checkout, and changes no browser or account setting; Booking.com records the page view. Listing a rate is not a hold or a guarantee, and prices can differ by session (member discounts when signed in). Run it on any https://www.booking.com page in an English-language session and do not navigate the tab while it runs. Dates the property cannot offer give status no_availability, and so can dates beyond the site's bookable window; for such dates the page may also apply the party the session last searched, which is reported as search_not_applied.",
inputSchema: {
type: "object", additionalProperties: false,
required: ["property_url", "check_in", "check_out", "adults", "currency"],
@@ -283,6 +292,44 @@
const facilities = pickTid(doc, "property-most-popular-facilities-wrapper");
const rules = rawText(pickTid(doc, "HouseRules-wrapper"));
const time = label => (new RegExp(label + "\\s*((?:From|Until|Between)\\s+\\d{1,2}(?::\\d{2})?\\s*(?:[AP]M)?(?:\\s*(?:-|to|\\u2013)\\s*\\d{1,2}(?::\\d{2})?\\s*(?:[AP]M)?)?)", "i").exec(rules) || [])[1] ?? null;
+ // Each house-rules row is a title cell and a content cell. Content lines are the innermost blocks with text;
+ // card logos are images named by alt text, and a crib or extra-bed row (an age or age range, then the offer and its
+ // charge, one cell per offer) is kept together as one line.
+ const ruleLine = s => s.length > 600 ? s.slice(0, 599) + "…" : s;
+ const ruleLines = (el, out) => {
+ if (out.length > 30 || el.getAttribute("aria-hidden") === "true") return out;
+ if (el.tagName === "IMG") { const alt = (el.getAttribute("alt") || "").replace(/\s+/g, " ").trim().slice(0, 300); if (alt) out.push(alt); return out; }
+ const kids = [...el.children];
+ if (kids.length >= 2 && /^\d+\s*(?:[-\u2013]\s*\d+|\+)?\s*years?(?:\s+old)?$/i.test(text(kids[0]))) {
+ out.push(ruleLine(text(kids[0]) + ": " + kids.slice(1).map(offer => ruleLines(offer, []).join(", ")).join("; ")));
+ return out;
+ }
+ if (!kids.length || [...el.childNodes].some(n => n.nodeType === 3 && n.textContent.trim())) {
+ const line = ruleLine(rawText(el));
+ if (line) out.push(line);
+ return out;
+ }
+ for (const kid of kids) ruleLines(kid, out);
+ return out;
+ };
+ // Rows are the siblings of the shallowest
, so a separator inside a row's content is not taken for one. Every
+ // property lists check-in and check-out, so rows without either are not the section's rows.
+ const rulesWrapper = pickTid(doc, "HouseRules-wrapper");
+ const depth = el => { let d = 0; for (; el; el = el.parentNode) d++; return d; };
+ const ruleSeparator = [...(rulesWrapper?.querySelector("[data-testid=\"property-section--content\"]")?.querySelectorAll("hr") || [])]
+ .reduce((top, hr) => !top || depth(hr) < depth(top) ? hr : top, null);
+ const ruleRows = [...(ruleSeparator?.parentNode.children || [])].filter(row => row.tagName === "DIV" && row.children.length >= 2 && text(row.children[0]));
+ const houseRules = [];
+ let ruleBytes = 0, rulesTruncated = false;
+ for (const row of ruleRows) {
+ const lines = ruleLines(row.children[1], []);
+ const rule = { title: text(row.children[0]), lines: lines.slice(0, 30) };
+ ruleBytes += new TextEncoder().encode(JSON.stringify(rule)).length;
+ if (houseRules.length === 20 || ruleBytes > 20000) { rulesTruncated = true; break; }
+ if (lines.length > 30) rulesTruncated = true;
+ houseRules.push(rule);
+ }
+ const rulesRead = houseRules.length > 0 && ruleRows.some(row => /^Check-(?:in|out)$/i.test(text(row.children[0])));
const property = {
property_id: cfg(html, /\bb_hotel_id\s*:\s*'(\d{1,20})'/),
name: name.slice(0, 300), property_url: query.property_url, page_url: query.property_url + "?" + params.toString(),
@@ -290,7 +337,7 @@
review: scoreMatch ? { score: Number(scoreMatch[1]), label: scoreMatch[2].trim(), count: Number(scoreMatch[3].replace(/,/g, "")) } : null,
review_categories: [...doc.querySelectorAll("[data-testid=\"review-subscore\"]")].slice(0, 20).map(el => /^(.*?),\s*([\d.]+),\s*Average rating/.exec(text(el))).filter(Boolean).map(m => ({ category: m[1], score: Number(m[2]) })),
popular_facilities: facilities ? [...facilities.querySelectorAll("li")].map(text).filter(Boolean).slice(0, 20) : [],
- check_in_time: time("Check-in"), check_out_time: time("Check-out")
+ check_in_time: time("Check-in"), check_out_time: time("Check-out"), house_rules: rulesWrapper ? (rulesRead ? houseRules : null) : [], house_rules_truncated: rulesRead && rulesTruncated
};
const rowEls = [...doc.querySelectorAll("#hprt-table tr.js-rt-block-row")];
@@ -303,7 +350,7 @@
const warnings = [];
if (signedIn) warnings.push("The browser is signed in to Booking.com, so prices may include member (Genius) discounts.");
if (ages.length && !agesConfirmed) warnings.push("The page did not list the child ages, so the ages sent could not be confirmed.");
- if (ages.length) warnings.push("Child charges and age limits are not checked; review the property's child policies.");
+ if (ages.length) warnings.push("Child charges and age limits are not checked against the rates; " + (property.house_rules?.length ? "see the property's child policies in property.house_rules." : "review the property's child policies on the property page."));
// A rate's capacity is in its occupancy cell ("Max. people: 2", "Max adults: 2 Max children: 1", "Only for 1
// guest") or, on another layout, in a "Guests: 2 adults, 1 child" line under its conditions. A child may take
// an adult place, as Booking.com's checkout counts guests.
diff --git a/registry.json b/registry.json
index 91949481..24d0d6a6 100644
--- a/registry.json
+++ b/registry.json
@@ -1132,11 +1132,11 @@
"https://www.booking.com/*"
],
"url_patterns_reason": "Narrowed to www.booking.com: the stays search and property pages are served there, and the read tools fetch those pages same-origin; a fetch from www.booking.com to flights. or secure.booking.com fails, so the tools cannot work on other booking.com hosts.",
- "status": "qa_passed",
- "last_verified": "2026-10-17",
- "notes": "Dated property facts and room rates with effective party and currency, each rate's adult and child places (max_adults, max_children) read from either of the two capacity layouts the site serves (an occupancy cell, or a 'Guests:' line under the rate's conditions), excluded fees, cancellation, prepayment, meals, rate IDs and comparison ranks, plus page_url, the dated property page select_room acts on. Partner Offers keep their row id (bbasic_0, _bbasic_0) and are marked partner_offer, with a warning that their price, terms and availability can change at checkout. Rates with fewer adult places than the party are left out (party_too_large); with children, a rate with places for the adults but not for every child is kept, marked fits_party false and ranked after the rates that fit (before max_rates), because Booking.com accepted such a rate for a baby (no checkout alert) but alerted 'accommodates fewer than 3 guests' for a 5-year-old; a rate listing no capacity is kept with a warning. Optional lowest-price ordering before the 40-rate cap; scans at most 400 rows. Independent QA on 2026-10-16 ran it live from the home and search pages on more than ten properties and on about a dozen sessions at Palacio Ramalhete with and without Partner Offers: partner flags and counts matched the page, room_id agreed with select_room, plus no-availability dates, currency mismatch, invalid input and past check_in. One room; English. Independent QA on 2026-10-17 (three rounds, final source 5d7822e) ran it live at a Lisbon property on both capacity layouts, compared row by row with the page: 2 adults with children aged 0, 1, 2, 5 and 12+5 (fits_party true, false and adult-short rates left out), 2 adults, a property listing no capacity (kept, warned), max_rates truncation with fitting rates ranked first under both sorts, and outputSchema conformance. Ages 3 and 4, Partner Offers in a child search and signed-in sessions were not tested.",
+ "status": "verified",
+ "last_verified": "2026-10-29",
+ "notes": "Adds house_rules: every house-rules section in page order with its title and lines (card logos by alt text, each crib or extra-bed row as one line, with several offers for one age range kept with their own charges), [] when the page has no house-rules section and null when it has one the tool cannot split into rows that include check-in or check-out. house_rules_truncated reports sections or lines left out by the 20-section, 30-line and about 20 KB bounds. The author ran an earlier revision live on 2026-10-28 on six Lisbon properties (pets allowed and not, cash accepted and not, cards-only and payment-method lists, crib and extra-bed rows, parties, quiet hours, no-age-restriction) and compared each section with the page; the final parser was then run against house-rules sections captured from 38 live Lisbon and Orlando property pages (single-age and multi-offer crib rows included), none truncated. Independent QA on 2026-10-29 of the final source (325e0ae), signed out in plain Kernel browsers started on the home page: search_stays then get_property at Palacio Ramalhete (2 adults, 28 days out) returned ok with all nine sections, including 'Pets are not allowed.', card logos as American Express, Visa and Mastercard next to 'Cash is not accepted', and the 0 - 2 years crib row as one line, untruncated. Bench booking-house-rules (5 pairs each, low thinking, tool from this source): Haiku 5.5 treatment vs control correct 5/5 vs 5/5 (1.2x faster), GPT-6 Luna 4/5 vs 2/5; 100% adoption, and every treatment trial that got get_property output answered correctly (9 of 9). The null and truncated paths are covered by fixtures only, and signed-in sessions were not tested. The rest is unchanged from the independently QA'd earlier source: dated property facts and room rates with effective party and currency, each rate's adult and child places (max_adults, max_children) read from either of the two capacity layouts the site serves (an occupancy cell, or a 'Guests:' line under the rate's conditions), excluded fees, cancellation, prepayment, meals, rate IDs and comparison ranks, plus page_url, the dated property page select_room acts on. Partner Offers keep their row id (bbasic_0, _bbasic_0) and are marked partner_offer, with a warning that their price, terms and availability can change at checkout. Rates with fewer adult places than the party are left out (party_too_large); with children, a rate with places for the adults but not for every child is kept, marked fits_party false and ranked after the rates that fit (before max_rates), because Booking.com accepted such a rate for a baby (no checkout alert) but alerted 'accommodates fewer than 3 guests' for a 5-year-old; a rate listing no capacity is kept with a warning. Optional lowest-price ordering before the 40-rate cap; scans at most 400 rows. Independent QA on 2026-10-16 ran it live from the home and search pages on more than ten properties and on about a dozen sessions at Palacio Ramalhete with and without Partner Offers: partner flags and counts matched the page, room_id agreed with select_room, plus no-availability dates, currency mismatch, invalid input and past check_in. One room; English. Independent QA on 2026-10-17 (three rounds, final source 5d7822e) ran it live at a Lisbon property on both capacity layouts, compared row by row with the page: 2 adults with children aged 0, 1, 2, 5 and 12+5 (fits_party true, false and adult-short rates left out), 2 adults, a property listing no capacity (kept, warned), max_rates truncation with fitting rates ranked first under both sorts, and outputSchema conformance. Ages 3 and 4, Partner Offers in a child search and signed-in sessions were not tested.",
"provenance": {
- "qa_agent": "agt_88d67e429f599403d4fb"
+ "qa_agent": "agt_f009472a350a7a79a637"
}
},
{
diff --git a/tests/booking-stays.test.mjs b/tests/booking-stays.test.mjs
index 83f071f2..a48deb1f 100644
--- a/tests/booking-stays.test.mjs
+++ b/tests/booking-stays.test.mjs
@@ -126,9 +126,25 @@ const partnerRow = ({ head = ROOM_A, blockId = "bbasic_0", total = "$298" } = {}
.replace('