Show only gas stations with fresh positive or probably positive evidence in the main list. Treat all variants of one octane number as one user-facing brand: base, premium, and branded 95 are output only as АИ-95, without separate lines 95+, "Ecto", G-Drive, and similar. Keep the original variant label internally only for provenance and conservative verification of negative data. Source failure, an empty sample, and no gasoline are different results. Always state the health of each source, freshness, and confidence, and also warn that page/crowdsourced data may be delayed. Each report also contains up to three forecasts of the nearest appearance based on accumulated history or an explicit message that there is not yet enough statistics.
Inviolable report format
Output report.mjs — the only canonical user-facing report. Publish its contents as ordinary Markdown without any changes: do not paraphrase, do not shorten, do not reorder lines, do not merge with your own list, and do not remove Markdown markup. In particular, every generated Yandex Maps link must reach the user unchanged.
Do not enclose the report in a code fence or blockquote: the links must remain clickable. Only a separate short service phrase before or after the full report is allowed if you need to explain a launch error; it does not replace or modify the report itself. If the report is received as JSON, decode the field markdown and publish exactly its value. Before sending, check that not a single line or link from the result report.mjs is lost.
Preparation
Determine the skill root by the location of this SKILL.md and use its absolute path as FUEL_WATCH_ROOT. The skill directory is a replaceable read-only artifact: never create or modify config, state, history, node_modules or other runtime files in it. User settings and accumulated data automatically live in ~/.fuel-watch/: config/, history/, state/ и monitors/. The variable FUEL_WATCH_HOME can relocate this entire directory. Do not change the user configuration without an explicit request.
First check the external runtime with the commands command -v agent-browser и agent-browser --version in the same environment where the collection will be run. If the command is available, use the installed version without reinstalling. Never run npm install -g agent-browser, npm update -g agent-browser, npx agent-browser or agent-browser installon your own. If the command is missing or does not start, do not install anything globally: return BROWSER_UNAVAILABLE, show the check result and ask the user for separate permission to modify the environment.
The runtime is shipped prebuilt in dist/ together with all Node.js dependencies. On the target machine, never run npm install, npm ci, npm update or npx for Fuel Watch. The absence of node_modules in an installed skill is normal. If dist/scripts/collect.mjs is missing, return a corrupted/incomplete skill package error; do not try to build it on the target machine.
In standard collection, sites are opened only through agent-browser, which are controlled by scripts. Do not add curl, fetch, a direct HTTP client, CAPTCHA solver, dashboard, HAR, video, trace, restore/profile or background browser.
Standard collection always uses HEADED Chromium without stealth flags or fingerprint spoofing. The only exception is the User-Agent: if the browser actually came up headless (on a server without a display this happens despite --headed) and the source responded to this with its own autolimit page, one retry is performed with the same browser identity but with the token removed Headless. The version and platform remain real, no other browser is impersonated, CAPTCHA is not bypassed; if desired, the string can be set explicitly via browser.userAgent. If a user display is available, BrowserRunner preserves DISPLAY/WAYLAND_DISPLAY/XAUTHORITY; otherwise agent-browser may use its own Xvfb, but the browser still remains non-headless. Do not transfer the result HEADLESSof diagnostics to a regular browser or to site availability in general. Each source gets a separate short-lived session, sources are processed strictly sequentially, so no more than one window is active at a time.
BrowserRunner first necessarily installs --allowed-domains. The behavior of network controls differs between versions of agent-browser, so the fallback is determined by the outcome, not by the error text. A source is retried once in a new owned session without controls if, with requested or active controls, agent-browser returned a known CDP error when installing them, does not know the flag, did not complete navigation, hung, or crashed again on open, the page immediately or later inside the adapter became about:blank/chrome-error, or the agent-browser command itself crashed (0.38.2 breaks real pages exactly like this, silently). Not retried: the response of the source itself (CAPTCHA, HTTP error, scheme change, a page that never became ready, a move to another origin), an adapter script error, and navigation blocking — agent-browser does not name the blocked URL, and it cannot be distinguished from a redirect outside the allowlist. Without controls, fail-closed checks of the exact final host and page drift are preserved, and the URL is checked before and after each data extraction (evalJson); an external login gateway to which the page went after openis also considered CHALLENGE. Such a result always receives the warning BROWSER_NETWORK_CONTROLS_DEGRADED; do not hide it and do not call the fallback equivalent to full network isolation.
The standard workflow does not manage tabs and does not collect visual artifacts: it only calls the scripts below. If the user explicitly asks to show the browser, inspect the DOM/network/console, or debug a source, first read references/browser-debugging.md and perform diagnostics separately from the collection-run.
One-time check
-
Create a temporary directory via
mktemp -d. -
Run:
node "$FUEL_WATCH_ROOT/dist/scripts/collect.mjs" --output "$RUN_DIR/snapshot.json" node "$FUEL_WATCH_ROOT/dist/scripts/report.mjs" --snapshot "$RUN_DIR/snapshot.json" -
Publish stdout
report.mjsin full and unchanged as ordinary Markdown even for code 2: it explains degradation and does not mean "there is no gasoline." Do not create your own summary instead of it. -
For code 75, warn about
CLEANUP_FAILED, then once check the cleanup of only the recorded owned namespace. Do not run a globalagent-browser close --allwithout a namespace. -
Delete the temporary directory. A compact history of statuses and hourly rolling-count signals by octane grades 92/95/98/100 is stored separately in the user state directory, is kept for
history.retentionDaysdays (90 by default; old days are deleted automatically) and is used by subsequent one-off checks, monitoring ticks, and statistics. The forecast in the report reads only the lastmin(retentionDays, 28)days; the entire history is available viahistory-stats.mjs(see below). Before comparing adjacent ticks, all variants of the same octane number are aggregated withinstation + source + tick: rolling-count is summed; status has priorityIN_STOCK → LIMITED → OUT_OF_STOCK → UNKNOWN, so an unreadable variant is ignored if there is at least one readable one. An octane transition is counted only with a witness — the same variant is present in both ticks and actually changedOUT_OF_STOCK → IN_STOCK/LIMITEDorcount 0 → count > 0; the appearance/disappearance of a row is not itself an event. Diesel, gas, unknown products, and other octane numbers are excluded. The path can be set explicitly via--history; the variableFUEL_WATCH_HISTORY_PATHchanges the default path.
Monitoring in the current task
Do not create automation, a daemon, or a new task. Monitoring is performed by the active agent in this dialogue until the user commands it to stop or there is an external interruption. If a background fuel-watch-collect.timeris active on the machine, do not start this mode — see "Background collection and history statistics."
-
Initialize the monitor and save the printed
monitorIdand absolutestateDirin the task context:node "$FUEL_WATCH_ROOT/dist/scripts/monitor.mjs" init -
Before the first and each subsequent tick, check
monitor.mjs due --state-dir "$STATE_DIR". When the tick arrives:- run
collect.mjs --previous "$STATE_DIR/previous.json"only if such a snapshot is prepared by the agent from the previous committed state; - create a report
report.mjs --snapshot "$SNAPSHOT" --state-dir "$STATE_DIR" --json; - call
monitor.mjs prepare --state-dir "$STATE_DIR" --report-id "$REPORT_ID" --snapshot "$SNAPSHOT"; - publish the value
markdownfrom the JSON in full, unchanged and not in a code fence; do not retell it and do not remove links; - only after successful publication call
monitor.mjs commit --state-dir "$STATE_DIR" --report-id "$REPORT_ID".
- run
-
Between ticks the browser is already closed. Wait with the foreground command
sleep 45or shorter. After each fragment check for new user input,monitor.mjs dueиSTOPthrough its fieldstopped; do not run a single 15-minute sleep. -
After four empty ticks use
report.mjs --compact, but continue the cadence. -
When resuming the task, first call
monitor.mjs recover --state-dir "$STATE_DIR". If there is a pending report, re-render it with--recovered, keep the same report ID and then commit. -
On stop call
node "$FUEL_WATCH_ROOT/dist/scripts/monitor.mjs" stop --state-dir "$STATE_DIR", perform one namespace-scoped cleanup of the last browser-run and finishnode "$FUEL_WATCH_ROOT/dist/scripts/monitor.mjs" cleanup --state-dir "$STATE_DIR". Confirm deletion of the monitor-state; general settings, the last snapshot and history are preserved.
Never keep the browser open while waiting. Any collection more often than the configured monitoring.intervalMinutes is prohibited in monitoring mode.
Background collection and history statistics
The long history on a Linux server is accumulated not by the agent, but by a systemd user timer: every monitoring.intervalMinutes it runs one collect.mjs without the agent, which appends a tick to the history and updates ~/.fuel-watch/state/latest.json. It is installed by the user (or the agent, but only at the user's direct request): sh "$FUEL_WATCH_ROOT/deploy/linux/install-systemd-user.sh", removal — with --uninstall, viewing unit files without installation — with --print. The script does not change the user config and does not require root; to work after logging out of the session, the administrator needs to enable once loginctl enable-linger. Do not create a timer, cron or other background task on your own initiative and do not run collect.mjs in a loop instead of the timer. Journal: journalctl --user -u fuel-watch-collect.service — one line per run with verdicts and source health.
While the timer is active (systemctl --user is-active fuel-watch-collect.timer prints active), the sources are already being polled at the configured frequency, and each extra collection doubles the load on them (benzonavt has already blocked IPs for frequent requests). Therefore, in this mode do not start monitoring in the task: for "what's now" and "watch" show the last snapshot report.mjs --snapshot ~/.fuel-watch/state/latest.json, stating the time it was collected, and on the next question — a fresh one again latest.json. Start a one-off collect.mjs only if the snapshot is older than two intervals (then first check systemctl --user status fuel-watch-collect.timer and tell about the lag) or the user explicitly asks to collect again.
Statistics — read-only, the browser does not start:
node "$FUEL_WATCH_ROOT/dist/scripts/history-stats.mjs" [--station "<часть названия, адреса или ключа>"] [--days N] [--limit N] [--json]
By default, Markdown is printed in Russian for physical gas stations (the keys of one gas station that have ever been encountered together are merged): the share of polls with AI-95 / without it / unclear, the current state and since when, the number of completed outages (NOT_AVAILABLE → confirmed positive without a collection gap longer than three intervals), the median and interquartile range of their duration, the distribution of return times by Moscow time hours and the typical window (only with three or more outages), as well as AI-95 arrivals based on reports from the sources themselves (a "no → yes" transition in reviews; messages from different sources and users within two hours count as one arrival; the "activity resumed" heuristic is not included here). The header contains the period, poll coverage, the longest collection gaps, and the share of OK status for each source. --json gives the same data plus the distribution by days of the week and the latest episodes for your own analysis.
Interpret honestly: the return time is the first poll that saw AI-95, accurate to the interval; an outage during which a collection gap occurred is not counted at all; a source that was for a long time in CHALLENGEas unavailable, it narrows the picture, and this must be stated. Do not present the window as a delivery schedule, do not draw conclusions from one or two interruptions, and do not mix this statistic with the current report: the canonical report is still published unchanged, while the statistic is a separate response.
Zone change
To verify the anchor coordinates, run the read-only command:
node "$FUEL_WATCH_ROOT/dist/scripts/resolve-area.mjs"
Show the found coordinates and drift to the user. Overwriting is allowed only after explicit confirmation: repeat with --write --confirm. Three non-unique or collinear points, incorrect coordinates, and an unresolved anchor result in fail-closed.
Interpretation
низкая,средняяивысокаяin the report always mean Fuel Watch's confidence in its own assessment, not a probability stated by the source. It is derived from the freshness and specificity of the evidence, provenance groups, and grade-specific activity. Do not attribute it to a separate service. Unverified source confidence/rating may be stored only as provenance; do not substitute it for the final confidence without formalized semantics and calibration.последний подтверждающий сигнал— the age of the freshest observation actually participating in the current positive verdict.только чтоmeans an age of less than one minute, and not necessarily a status change or the arrival of a fuel truck. The sources of the current assessment and the sources of the historical transition are shown on separate lines; do not combine them into a single label.ЕСТЬ: a fresh direct signal of any AI-95 variant.СКОРЕЕ ЕСТЬ: a family/indirect positive signal or a confirmed grade-specific resumption of activity. In the user report and history, do not separate variants of the same octane number.- Do not call a generic signal a transaction.
TRANSACTIONS_RESUMED— only a grade-specific gap of at least 60 minutes and at least two new events within 20 minutes. - Do not interpret the update time as the delivery time. "Appeared between…" is acceptable only for a confirmed transition from
NOT_AVAILABLE; otherwise say "first seen" or "time of appearance unknown". - Forecast — not a fact of delivery. The strongest permissible training signal is the typical local time of transition of the present grade-specific rolling-count
0 → ≥2by gasoline grades 92/95/98/100 after a one-hour quiet window. Do not multiply the aggregate station counter by grades: the live Yandex currently returns exactly the aggregatesignalsCountPerHour, so it is excluded from the forecast as potentially containing diesel signals. On the current data, the main available sign of overall supply is the simultaneous transition of several gasoline statuses fromOUT_OF_STOCKвIN_STOCK/LIMITED; a single transition is weaker. Diesel, gas, and unknown products are not taken into account. Confirmed transitions of the union AI-95 serve as the last fallback. First use the history of this station, then of the brand, then of the zone; always show the interval, signal type, number of episodes, and confidence. Do not invent a time on a cold start and do not hide a lack of data. - One-off checks also accumulate this history. Monitoring is not mandatory, but frequent ticks give substantially better temporal resolution; rare manual runs may completely miss a short rolling-count transition.
- A presence-only queue is not compared as short. An unknown queue is sorted after known ones.
- Yandex, gdebenz, and benzonavt belong to the same provenance group by default and by themselves do not give an independent multi-source increase in confidence.
- У Yandex station-level
lastSignalTimestampиsignalsCountPerHourdo not relate to a specific grade and must not give it freshness or increase confidence. Grade observation uses only the timestamp/count from the grade row itself; aggregate time is permissible for the queue and station context. If the live payload does not contain grade-time, the source honestly returnsPARTIAL/NO_GRADE_FRESHNESS_METADATA, and notCOMPLETENESS_INVARIANTand not a fresh verdict. - У Benzonavt
fuels_nowis treated as an exhaustive current list: a nonempty list without 95 — family-negative. Anyst.conflictbefore its semantics are formalized transfers the fuel observation toUNCERTAINand prohibits both a recommendation and family-wide negation.queue.size=20_50/gt50display asLONG/VERY_LONG, using its ownqueue.at. - The 2GIS fuel catalog list does not prove current availability; consider only an explicit current-status with
last_report_at. CAPTCHA meansCHALLENGE; do not try to solve it. - The fuel station assortment (
assortment,sellsRequestedFamily) answers a different question than availability: “95 is not sold here at all” is not a shortage. Such stations are counted as a separate line “does not sell AI-95” and do not receive a delivery forecast. Assortment is published only by 2GIS, Benzonavt, and some gdebenz lines; the absence of a catalog means “unknown,” not “not sold,” thereforesellsRequestedFamily: undefinedchanges nothing in the verdict. лимит: N лin the station line is the minimum known limit on AI-95. Only the observation of the limit goes stale (observedAt, for examplelast_atat gdebenz): a limit seen long ago may have disappeared, and it cannot be included in the minimum. A limit about which the source says “effective from” (inForceSinceat Benzonavt) or which is published without a date (2GIS) remains in force regardless of the age of that date — discarding it would mean understating the trip constraint. Next to an observed and no longer fresh limit, the age is printed:лимит: 20 л (3 ч назад). This is a trip constraint, not an indication of availability.trustу observation (confidence,confirmations,reports,basis,reports_count,author_*) — the source’s own stated confidence. This is provenance for diagnostics; never present it as Fuel Watch confidence and never show it to the user as a probability.- A discrepancy between the visible card and the hidden source field is a structural conflict, not a reason to choose the “convenient” field. At gdebenz, a brand named only in
fuels_nowor only in the card text yieldsUNCERTAINwhile preserving both wordings inconflict, and such a positive does not go into “Where to go.” Another source with its own direct positive for this fuel station remains in force. renderReportreturns a separate fielddiagnostics(agentOnly: true) with the full list of warnings, source health, runtime, identity diagnostics, and orphaned processes. This is internal data: do not quote it to the user and do not present it as a trip limitation. Limitations affecting the trip are already printed in the canonical Markdown in human language.- The history links a physical gas station by saved member IDs, not only by the volatile merged
stationKey, and serializes concurrent read–modify–write through a bounded lock.HISTORY_LOCK_TIMEOUTmeans an unavailable forecast, but does not cancel the current report.
One-off arbitrary geography
collect.mjs --area <файл> --output <файл> collects one zone for one run. The configured polygon, ~/.fuel-watch/state/latest.json and the shared history are not touched, therefore --output is required. The file contains a zone object (or {"area": {...}}) of any supported kind, including route-corridor with explicit waypoints и corridorWidthMeters, and is validated by the same schema as the configured zone, so an error comes with the field name. A zone may declare its own ceilings maxAreaSquareKm и maxStationCount: exceeding the area stops the run before polling sources, exceeding the number of stations — immediately after identity reconciliation. This is not a new default: the permanent zone changes only via resolve-area.mjs.
Example
Request: “Where is 95 available right now, and where is the queue shorter?”
Expected result: one report with timestamp and zone, health of Yandex/gdebenz/2GIS/Benzonavt, changes if there is a previous tick, ranked positive gas stations only, a single aggregated line АИ-95 with freshness and confidence for each station, separate counters of negative/unknown data, a forecast of up to the three nearest appearances, and a mandatory warning.
Errors
CHALLENGE,HTTP_ERROR,TIMEOUT,RESOURCE_BLOCKED,SCHEMA_CHANGED,NAVIGATION_FAILED,PAGE_LOST: name the source and continue with the others. The report already prints the line “Unavailable sources: …” with a human-readable reason and lists which sources the estimate was built from; publish it as is.- All sources degraded: report “no available fresh data”, not “no gasoline”.
CHALLENGEalso means that the source sent the browser to an external login or verification page, which must return it back (for example, 2GIS →id.sber.ruсredirect_urito 2gis.ru; this is how 2GIS responds specifically to automated Chrome). The skill does not log into the account, does not repeat such a visit and does not read someone else's page; landing on a foreign domain with no way back atopenremainsRESOURCE_BLOCKED, and a late departure to another origin isPAGE_LOST.CHALLENGEand user-in-the-loop: the skill never solves or bypasses CAPTCHA. If the config hasbrowser.challengeHandoverenabled (disabled by default and works only in headed mode), collect holds the already open window for a limited time so that a human can solve the verification themselves, only re-reads the page and retries the source once. Neither clicks, nor input, nor form submission happen on the part of the skill. The outcome is recorded inruntime.challengeHandovers:CLEARED— the verification page disappeared (we do not observe who exactly removed it, and one cannot assert that),TIMED_OUT— did not disappear within the allotted time,NOT_OBSERVABLE— the adapter recognized verification from the page data, but there is no visible marker, so the window was not held,UNREADABLE— the page was no longer readable already at the first attempt, so the window was not held,LOST_WHILE_HELD— the window was held, but the page stopped being readable during the wait. The limit ofmaxPerRunis consumed by all outcomes in which the window was actually held:CLEARED,TIMED_OUTиLOST_WHILE_HELD.HTTP_429_LIMITEDfor Yandex: the source returned the autolimit pagelimited. Collect already makes one retry with the same browser identity without the tokenHeadless; if the code still reached the report, this is a real limit, not a lack of fuel. Do not bypass it with new attempts and do not substitute stealth flags.- Orphaned browsers: a clean
closedeletes the namespace pid file, therefore a live agent-browser process still recorded under the namespace created by this run is an orphan. Such processes end up inruntime.orphanProcessesand by default are terminated (SIGTERM, thenSIGKILL) within the same cleanup budget;browser.reapOrphanProcesses: falseleaves only the record. The signal is sent only if three conditions match: the pid was recorded under the namespace created by this run, it is the same pid that we saw serving the namespace during the run, and the process's executable file is actually namedagent-browser*. There is no general process scan: it would affect browsers of other sessions on the same machine. CLEANUP_FAILED/ code 75: the data can be shown, but a warning is mandatory; perform one repeated namespace-scoped cleanup check before waiting or a new collection.- Invalid configuration / code 2 before browser startup: show the exact JSON path from the error and do not collect anything.
HISTORY_UNAVAILABLE: the current data can be shown, but the forecast is unavailable; name the write/read error and do not substitute it with a made-up time.- The temporary state was not deleted: report the absolute directory and do not claim that monitoring has been fully stopped.
Development check
After changing the skill, run npm test. Live smoke tests are separated and run only on explicit request: npm run test:live. They never enter the 15-minute monitoring loop.