Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion skills/wego/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,7 @@ wego flights results <searchId> \
[--currency USD] [--locale en]
```

Translate the user's constraints into flags – every one of them, on this read. Re-supply still-active constraints on each refinement instead of assuming a previous filtered read changed the underlying search. Use `--stops 0` for nonstop.
Translate the user's constraints into flags – every one of them, on this read. Re-supply still-active constraints on each refinement instead of assuming a previous filtered read changed the underlying search. Use `--stops 0` for nonstop and `--stops 0,1` for at most one stop – the flag selects a set, never a ceiling (see below).

- `--wait` performs a bounded server-friendly settle and adds a `settled: converged|budget_exhausted` field to the output; prefer it over hand-rolled re-read loops.
- **The clock filters cover both ends of both legs.** Eight flags, one per `{outbound, return} × {departure, arrival} × {blocks, range}` combination. `--departure-*` and `--arrival-*` bound the **outbound** leg; prefix `--return-` for the return leg. Each reads local time at **that** airport, so `--arrival-range` is the arrival airport's clock, not the origin's.
Expand All @@ -238,6 +238,7 @@ Translate the user's constraints into flags – every one of them, on this read.
- `--outbound-min-duration` / `--outbound-max-duration` / `--return-min-duration` / `--return-max-duration` bound **one leg's** elapsed time in minutes, inclusive. These are always leg-prefixed because the unprefixed `--max-duration` bounds the **whole trip**, and the two answer different questions: a 3-hour outbound paired with a 14-hour return totals 17 hours, so no trip-wide ceiling can reject the long way home on its own. Use the per-leg flags for "I do not mind a long flight out but keep the return under 8 hours" (`--return-max-duration 480`).
- `--booking-types` (`wego`, `airline`) accepts comma-separated enum values; like `--sort`, it is validated client-side before any network call, so a typo fails locally.
- `--alliances` takes comma-separated codes in the server's own spelling, and is **not** a fixed set, so it is not validated locally: an unknown code returns an empty page rather than an error. Read `metadata.filterOptions.alliances` for the codes the current snapshot actually carries. Common ones are `star_alliance`, `oneworld` and `sky_team` (note the underscore), alongside groupings that are not strictly alliances, such as `lcc` for low-cost carriers.
- **`--stops` selects an exact set of stop counts, it does not bound them.** The values are OR'd, so `--stops 1` keeps trips with exactly one stop and **silently drops every nonstop** – on a route where the nonstops are the fastest itineraries, that discards the best options while still returning a large `totalCandidates` and no warning. There is no maximum form of this flag: express "at most N stops" by enumerating every count from 0, so "at most one stop" is `--stops 0,1` and "at most two" is `--stops 0,1,2`. Whenever the traveller says "at most", "no more than", "up to" or "direct if possible", that enumerated list is the flag, not the bare number they said. The count is **trip-level** – the maximum across the legs, the same value each card's `stops` field publishes – so a nonstop outbound with a one-stop return counts as one and is kept by `--stops 1`; do not fold `legs[]` yourself. Unlike `--alliances` and `--aircraft` there is **no `metadata.filterOptions.stops`** vocabulary to read, so enumerate from 0 rather than trying to discover the counts this snapshot carries. The counts are **unbounded and not bucketed** – 2 means exactly two, never "two or more", and a thin route can carry 3- and 4-stop trips as separate counts. So an enumerated ceiling can come back `totalCandidates: 0` because the route's *floor* is above it, not because the route has no flights: on a poorly connected pair the cheapest itinerary may be a 2-stop one, and `--stops 0,1` correctly matches nothing. Read that as "no trip is this direct", widen the enumeration, and say which ceiling you had to relax – never report it as "no flights found".
- `--min-stopover-duration` / `--max-stopover-duration` bound the layover in **minutes**, inclusive, on a trip's **worst leg** – the largest leg total, never an individual connection. `--max-duration` bounds neither, because it adds flying and waiting into one number. The ceiling is exact about what it measures, since capping the worst leg caps every leg – but what it measures is **how long** a wait is, never **when** it falls. It is not an overnight filter: a 135-minute wait starting 04:00 is under any sane ceiling and is still a night in the terminal, and a 465-minute wait starting 11:00 is over it and never sees one. For "no overnight wait", bound the length with this flag if the traveller also wants it short, then read the connection's own clock – `flights trip <tripId> --view detail` gives each segment's `arrivesAt` and `departsAt`, and the gap between them is the wait, in local time at the connection airport. **The floor is weaker than it sounds**: a leg waiting 450 then 510 minutes is judged on 960, so `--min-stopover-duration 120` does not promise every connection is 120 minutes long – on a multi-stop leg, or on the shorter leg of a round trip, a tight connection can survive it. When the traveller says "leave me at least two hours to change planes", send the floor **and then read the card**: `legs[].layoverMinutesByStop` lists one wait per connection, aligned to `via`, so you can see the real gaps and say so rather than implying a guarantee the filter does not make. A direct trip totals 0, so every maximum keeps it and any minimum above 0 drops it – pair a floor with `--stops 0` only when the traveller wants both. Read `metadata.filterOptions.stopoverDurations` (`{min, max}`) for the span the snapshot carries before picking a bound.
- `--stopover-airports` (comma-separated IATA codes, e.g. `DOH`) restricts connections to those airports – use it for "connect through Doha" or "must have a stopover in X".
- `--aircraft` takes comma-separated aircraft **codes** in the server's own spelling (`380`, `789`, `32N`), not the labels the cards print (`A380`, `B787-9`, `A320 Neo`). It is not a fixed set and is not validated locally: an unknown code returns an empty page rather than an error. A trip matches when **any** leg flies a listed code, so an A380 outbound with an A320 return still matches, and there is no whole-trip variant of this flag the way `--airlines-match all` is for airlines. Read `metadata.filterOptions.aircraft` for the codes this snapshot carries and the label beside each one, then filter on the code – several codes can share one label (`321` and `32S` are both `A321`), so the label alone cannot address them. Use it for a positive requirement - "I want the A380", "put me on a 787" - and never fold `results[].legs[].aircraft` yourself to answer that: the page is one slice of the ranked candidate set, so the cheapest few cards are not where a widebody has to appear. **It cannot express an exclusion.** "Anything but a regional jet" has no flag: passing the codes the traveller wants to AVOID selects exactly the trips they refused. Either enumerate the acceptable codes from `metadata.filterOptions.aircraft` and pass those, or say the API cannot filter that way and let the traveller choose from what the snapshot carries.
Expand Down
2 changes: 1 addition & 1 deletion src/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3964,7 +3964,7 @@ export const RESULTS_USAGE = usage({
],
["--same-airline true", "One airline for the whole trip."],
["--alliances star_alliance", "Alliance codes, lowercase. Comma-list."],
["--stops 0,1", "Max stops on the trip. Comma-list."],
["--stops 0,1", "Exact stop counts. At most 1 is 0,1."],
["--min-price N", "Whole trip."],
["--max-price N", "Whole trip."],
["--max-duration N", "Whole trip, minutes."],
Expand Down
19 changes: 19 additions & 0 deletions src/help-shape.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,4 +163,23 @@ describe("help shape", () => {
expect(UNINSTALL_USAGE).toMatch(/--dir skill install is not touched/);
expect(TELEMETRY_USAGE).toContain("WEGO_CLI_TELEMETRY");
});

// Issue #105. `?stops=` is a set filter: the values are OR'd against the
// trip-level stop count, so `--stops 1` selects one-stop trips and drops
// every nonstop. Calling it a "max" sent callers to the one spelling that
// silently discards the fastest itineraries on the route, and the failure is
// invisible downstream - the page is valid, totalCandidates is large, and
// nothing says a category was removed. Pin BOTH halves of the fix: that the
// bound word is gone, and that the enumerate-from-0 recipe is present. A
// description that only stopped lying would leave the flag correct and still
// unusable for the constraint callers actually state.
it("--stops reads as a set with the at-most recipe, never as a bound", () => {
const line = RESULTS_USAGE.split("\n").find((l) =>
l.startsWith(" --stops "),
);
expect(line).toBeDefined();
expect(line).not.toMatch(/\b(Max|Maximum|Up to|No more than)\b/);
expect(line).toContain("Exact stop counts");
expect(line).toContain("0,1");
});
});
12 changes: 12 additions & 0 deletions src/skill-embed.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,18 @@ describe("readEmbeddedSkill", () => {
expect(body).toContain("Operating contract");
expect(body.length).toBeGreaterThan(500);
});

// Issue #105. The help text was the only surface describing `--stops`, and it
// described it backwards; the skill named just `--stops 0`, the single value
// where "exact set" and "maximum" agree, so it never contradicted the help.
// The skill is what an agent reads, so it has to carry the enumerated form -
// and unlike --alliances/--aircraft there is no metadata.filterOptions.stops
// to fall back on, which leaves these two surfaces as the only sources.
it("teaches the enumerated `at most N stops` form of --stops", async () => {
const body = await readEmbeddedSkill();
expect(body).toContain("`--stops 0,1`");
expect(body).toMatch(/`--stops` selects an exact set/);
});
});

describe("SKILLS registry", () => {
Expand Down
Loading