From eed89877d1804f4b3cf1df90b7c0d7012b4f5865 Mon Sep 17 00:00:00 2001 From: "Aaron K. Clark" Date: Tue, 13 Oct 2026 06:41:26 -0500 Subject: [PATCH 1/2] fix(agent): answer Workday's My Information instead of handing it back every round (#453) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On finastra.wd3 the walk signed in, filled the page, and handed back the same five fields on every round. Four causes: - Workday's search box ("How Did You Hear About Us?") says "Search" in its placeholder, so discovery read it as a site search and dropped it while the required-empty sweep named it anyway. A selectinput box is a question now, and it is typed into key by key before Enter, which is the only way Workday's widget hears it. - The sweep keyed a box by id and discovery by name, so City was unmapped under address--city and never drafted under city. The sweep now keys name then id, as discovery does. - Phone Device Type was discovered before its menu was read, and the phone rule drafted the number into it. The drafter answers the kind of phone (Mobile, or the mobile / cell / home option) first. - The account's email is drawn as text with no box: an address already on the page counts as answered. The drafter also answers where the posting was seen (LinkedIn, or the job-board option) instead of leaving it to the person, and the city and state of the résumé's location as their own boxes, with a US abbreviation expanded for a fixed list. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_015Xrsyx9xJYfnT2BzLMNFsB --- .../AnswerDrafterTests.cs | 23 ++++++- .../BrowserSubmitterTests.cs | 32 ++++++++++ api/ApplyTrack.Api/Agent/AnswerDrafter.cs | 62 ++++++++++++++++++- .../Agent/Browser/BrowserSession.cs | 3 + .../Agent/Browser/BrowserSubmitter.cs | 16 +++-- 5 files changed, 130 insertions(+), 6 deletions(-) diff --git a/api/ApplyTrack.Api.Tests/AnswerDrafterTests.cs b/api/ApplyTrack.Api.Tests/AnswerDrafterTests.cs index b2c2307..c0f0b37 100644 --- a/api/ApplyTrack.Api.Tests/AnswerDrafterTests.cs +++ b/api/ApplyTrack.Api.Tests/AnswerDrafterTests.cs @@ -348,7 +348,8 @@ public void Human_only_and_missing_facts_are_flagged_with_a_pointer() { var ctx = new AnswerContext(new Resume(), new AgentSettings(), "", "", ""); var (a1, r1) = AnswerDrafter.Deterministic( - new("q", "How did you hear about us?", true, PacketQuestion.Text, [], PacketQuestion.Custom), ctx); + // "How did you hear about us?" is answered since #453; a start date is still the person's. + new("q", "What is your earliest start date?", true, PacketQuestion.Text, [], PacketQuestion.Custom), ctx); Assert.Null(a1); Assert.Contains("only you", r1); @@ -580,4 +581,24 @@ public void A_consent_question_naming_a_phone_or_email_is_not_that_field(string public void A_plain_phone_label_still_takes_the_number(string label) => Assert.Equal(("555-0100", null), AnswerDrafter.Deterministic( new("question_2", label, true, PacketQuestion.Text, [], PacketQuestion.Custom), Ctx())); + + /// Workday's My Information as finastra.wd3 asks it (#453): the kind of phone, where + /// the posting was seen, and the city and state of the résumé's location as their own boxes. + [Fact] + public void Workday_my_information_boxes_get_the_phone_kind_the_source_and_the_city_and_state() + { + var ctx = Ctx(); // Location = "Lincoln, NE" + static PacketQuestion Q(string id, string label, params string[] options) => new(id, label, true, options.Length > 0 ? PacketQuestion.Select : PacketQuestion.Text, [.. options], PacketQuestion.Custom); + Assert.Equal(("Mobile", null), AnswerDrafter.Deterministic(Q("phoneType", "Phone Device Type"), ctx)); + Assert.Equal(("Mobile", null), AnswerDrafter.Deterministic(Q("phoneType", "Phone Device Type", "Fax", "Landline", "Mobile"), ctx)); + Assert.Equal(("Home Cellular", null), AnswerDrafter.Deterministic(Q("phoneType", "Phone Device Type", "Home", "Home Cellular", "Work"), ctx)); + Assert.Equal(("LinkedIn", null), AnswerDrafter.Deterministic(Q("source--source", "How Did You Hear About Us?"), ctx)); + Assert.Equal(("Job Board", null), AnswerDrafter.Deterministic(Q("source", "How did you hear about us?", "Event", "Job Board", "Social Media"), ctx)); + Assert.Equal(("Lincoln", null), AnswerDrafter.Deterministic(Q("city", "City"), ctx)); + Assert.Equal(("NE", null), AnswerDrafter.Deterministic(Q("countryRegion", "State"), ctx)); + Assert.Equal(("Nebraska", null), AnswerDrafter.Deterministic(Q("countryRegion", "State", "Kansas", "Nebraska", "Iowa"), ctx)); + // The phone number is still the number, and a location box still takes the whole place. + Assert.Equal(("5551234567", null), AnswerDrafter.Deterministic(Q("phoneNumber", "Phone Number"), ctx with { Settings = new AgentSettings { Phone = "5551234567" } })); + Assert.Equal(("Lincoln, NE", null), AnswerDrafter.Deterministic(Q("location", "Current location"), ctx)); + } } diff --git a/api/ApplyTrack.Api.Tests/BrowserSubmitterTests.cs b/api/ApplyTrack.Api.Tests/BrowserSubmitterTests.cs index 906efe5..6872948 100644 --- a/api/ApplyTrack.Api.Tests/BrowserSubmitterTests.cs +++ b/api/ApplyTrack.Api.Tests/BrowserSubmitterTests.cs @@ -581,6 +581,8 @@ function suggest(box) { // The same wizard as finastra.wd3 serves it (#450): step 1 opens on Create Account, drawn a // while after the page, with a "Sign In" link that turns the form over to the sign-in. _fixture.MapGet("/wd/en-US/Careers/job/Omaha/Engineer_R2/apply/applyManually", () => Results.Content(WorkdayWizardCreateFirstHtml, "text/html")); + // Workday's search box beside a real site search (#453): the box is a question, the search is not. + _fixture.MapGet("/jobs/workday-search-box", () => Results.Content(WorkdaySearchBoxHtml, "text/html")); _fixture.MapPost("/wd/step", async (HttpRequest req) => { using var reader = new StreamReader(req.Body); @@ -1513,6 +1515,21 @@ Click the object that does not fit the column pattern """; + private const string WorkdaySearchBoxHtml = """ + +
+
+ +
+
    + +
    + + +
    + + """; + private const string WorkdayWizardChoicesHtml = """

    Sign In

    @@ -3163,6 +3180,21 @@ public async Task A_workday_wizard_that_opens_on_create_account_is_turned_over_t Assert.Contains(steps, s => s.StartsWith("step4 ")); } + /// Workday's search box says "Search" in its placeholder and was read as a site search + /// and dropped (#453); the site's own search box still is. + [SkippableFact] + public async Task A_workday_search_box_is_discovered_as_a_question_and_the_site_search_is_not() + { + Skip.IfNot(Available, "Node Playwright is not installed (npm ci)"); + var discoverer = new FormDiscoverer(new BrowserOptions { Endpoint = _ws, AllowPrivateTargets = true, TimeoutSeconds = 60 }, NullLogger.Instance); + var questions = await discoverer.DiscoverAsync($"{_fixtureUrl}/jobs/workday-search-box"); + Assert.NotNull(questions); + var source = Assert.Single(questions!, q => q.Id == "source--source"); + Assert.Equal(("How Did You Hear About Us?", true), (source.Label, source.Required)); + Assert.Contains(questions!, q => q.Id == "city"); // keyed by name, as the sweep keys it + Assert.DoesNotContain(questions!, q => q.Id == "q" || q.Label.Contains("Search for Jobs")); + } + [SkippableFact] public async Task A_workday_posting_with_no_saved_account_names_the_host_that_needs_one() { diff --git a/api/ApplyTrack.Api/Agent/AnswerDrafter.cs b/api/ApplyTrack.Api/Agent/AnswerDrafter.cs index ce7f48d..7ef3386 100644 --- a/api/ApplyTrack.Api/Agent/AnswerDrafter.cs +++ b/api/ApplyTrack.Api/Agent/AnswerDrafter.cs @@ -70,12 +70,51 @@ public sealed partial class AnswerDrafter private static partial Regex LastName(); [GeneratedRegex(@"^full name$|^name$|your name", RegexOptions.IgnoreCase)] private static partial Regex FullName(); - [GeneratedRegex(@"how did you hear|\breferr|\bstart date|available to start|notice period|\bearliest|captcha", RegexOptions.IgnoreCase)] + [GeneratedRegex(@"\breferr|\bstart date|available to start|notice period|\bearliest|captcha", RegexOptions.IgnoreCase)] private static partial Regex HumanOnly(); // Anchored, and checked AFTER the eligibility rules: "…to remain in your current // location?" is a sponsorship question that once got a city typed into it. [GeneratedRegex(@"^(?:current )?location\b|\(city\)|^city\b|^(?:what is )?your (?:current )?location\b|where (?:are you|do you) (?:based|located|live)", RegexOptions.IgnoreCase)] private static partial Regex LocationRe(); + + /// "Minden, Nebraska (Remote)" → "Minden": the city of the résumé's location (#453). + public static string LocationCity(string location) + { + var parts = StripLocationSuffix(location).Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + return parts.Length == 0 || parts[0].Equals("remote", StringComparison.OrdinalIgnoreCase) ? "" : parts[0]; + } + + /// "Lincoln, NE 68508" → "NE": the state of the résumé's location, postal code dropped (#453). + public static string LocationState(string location) + { + var parts = StripLocationSuffix(location).Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + if (parts.Length < 2) return ""; + var state = Regex.Replace(parts[1].Trim().TrimEnd('.'), @"\s+\d[\d-]*$", "").Trim(); + return UsStates.Contains(state) || parts.Length == 2 ? state : ""; + } + + private static readonly Dictionary UsStateNames = new(StringComparer.OrdinalIgnoreCase) + { + ["AL"] = "Alabama", ["AK"] = "Alaska", ["AZ"] = "Arizona", ["AR"] = "Arkansas", ["CA"] = "California", ["CO"] = "Colorado", + ["CT"] = "Connecticut", ["DE"] = "Delaware", ["DC"] = "District of Columbia", ["FL"] = "Florida", ["GA"] = "Georgia", ["HI"] = "Hawaii", + ["ID"] = "Idaho", ["IL"] = "Illinois", ["IN"] = "Indiana", ["IA"] = "Iowa", ["KS"] = "Kansas", ["KY"] = "Kentucky", ["LA"] = "Louisiana", + ["ME"] = "Maine", ["MD"] = "Maryland", ["MA"] = "Massachusetts", ["MI"] = "Michigan", ["MN"] = "Minnesota", ["MS"] = "Mississippi", + ["MO"] = "Missouri", ["MT"] = "Montana", ["NE"] = "Nebraska", ["NV"] = "Nevada", ["NH"] = "New Hampshire", ["NJ"] = "New Jersey", + ["NM"] = "New Mexico", ["NY"] = "New York", ["NC"] = "North Carolina", ["ND"] = "North Dakota", ["OH"] = "Ohio", ["OK"] = "Oklahoma", + ["OR"] = "Oregon", ["PA"] = "Pennsylvania", ["RI"] = "Rhode Island", ["SC"] = "South Carolina", ["SD"] = "South Dakota", ["TN"] = "Tennessee", + ["TX"] = "Texas", ["UT"] = "Utah", ["VT"] = "Vermont", ["VA"] = "Virginia", ["WA"] = "Washington", ["WV"] = "West Virginia", + ["WI"] = "Wisconsin", ["WY"] = "Wyoming", + }; + // Workday's My Information asks these as their own boxes (#453): the city and state of + // the résumé's location, the kind of phone, and where the posting was seen. + [GeneratedRegex(@"^city\b|^town\b", RegexOptions.IgnoreCase)] + private static partial Regex CityRe(); + [GeneratedRegex(@"^state\b|^province\b|state ?/ ?province|state or province|^region\b", RegexOptions.IgnoreCase)] + private static partial Regex StateRe(); + [GeneratedRegex(@"(?:phone|device|number) type|type of (?:phone|number)", RegexOptions.IgnoreCase)] + private static partial Regex PhoneTypeRe(); + [GeneratedRegex(@"how did you hear|how you heard|hear about (?:us|this|the)|source of (?:this )?application|referral source", RegexOptions.IgnoreCase)] + private static partial Regex HeardAboutRe(); [GeneratedRegex(@"^country\b|country of residence|(?:which|what) country|(?:choose|select|enter) (?:your )?(?:current )?(?:location |residence |home )?country\b", RegexOptions.IgnoreCase)] private static partial Regex CountryRe(); @@ -223,6 +262,11 @@ public static (string? Answer, string? Reason) Deterministic(PacketQuestion q, A var consent = Consent().IsMatch(label); if (id is "email" || (EmailRe().IsMatch(label) && !consent)) return ctx.Email.Length > 0 ? (ctx.Email, null) : (null, "no email on the account"); + if (PhoneTypeRe().IsMatch(label)) + return q.Options.Count == 0 ? ("Mobile", null) + : q.Options.FirstOrDefault(o => Regex.IsMatch(o, @"mobile|cell", RegexOptions.IgnoreCase)) is { } mobile ? (mobile, null) + : q.Options.FirstOrDefault(o => Regex.IsMatch(o, @"\bhome\b", RegexOptions.IgnoreCase)) is { } home ? (home, null) + : (null, "pick the kind of phone yourself"); // A menu that names a phone — Workday's "Phone Device Type" (Home / Home Cellular) — is // not the number (#443): its choice is picked below like any other fixed set. if (id is "phone" || (Phone().IsMatch(label) && !consent && q.Options.Count == 0)) @@ -253,6 +297,22 @@ public static (string? Answer, string? Reason) Deterministic(PacketQuestion q, A return YesNo(q, ctx.Settings.ClearanceOk, "Settings · Agent says whether you hold a clearance"); // "Minden, Nebraska (Remote)" is how a résumé says it; "Minden, Nebraska" is what a // form's city autocomplete can geocode. The suffix is for the reader, not the form. + if (HeardAboutRe().IsMatch(label)) + return q.Options.Count == 0 ? ("LinkedIn", null) + : q.Options.FirstOrDefault(o => o.Contains("linkedin", StringComparison.OrdinalIgnoreCase)) is { } li ? (li, null) + : q.Options.FirstOrDefault(o => Regex.IsMatch(o, @"job board|job site|online|internet|social", RegexOptions.IgnoreCase)) is { } board ? (board, null) + : (null, "say where you heard about this one"); + if (CityRe().IsMatch(q.Label)) + return LocationCity(ctx.Resume.Location) is { Length: > 0 } city ? (city, null) : (null, "add a location in Résumé settings"); + if (StateRe().IsMatch(q.Label) && !CountryRe().IsMatch(q.Label)) + { + var state = LocationState(ctx.Resume.Location); + if (state.Length == 0) return (null, "add a location with its state in Résumé settings"); + if (q.Options.Count == 0) return (state, null); + var full = UsStateNames.TryGetValue(state, out var name) ? name : state; + return q.Options.FirstOrDefault(o => o.Equals(full, StringComparison.OrdinalIgnoreCase) || o.Equals(state, StringComparison.OrdinalIgnoreCase)) is { } opt ? (opt, null) + : (null, "pick the state option yourself — none reads as " + full); + } if (id is "location" || LocationRe().IsMatch(q.Label)) return StripLocationSuffix(ctx.Resume.Location) is { Length: > 0 } loc ? (loc, null) : (null, "add a location in Résumé settings"); if (id is "country" || CountryRe().IsMatch(q.Label)) diff --git a/api/ApplyTrack.Api/Agent/Browser/BrowserSession.cs b/api/ApplyTrack.Api/Agent/Browser/BrowserSession.cs index 247d5ea..35ce7bc 100644 --- a/api/ApplyTrack.Api/Agent/Browser/BrowserSession.cs +++ b/api/ApplyTrack.Api/Agent/Browser/BrowserSession.cs @@ -62,6 +62,9 @@ public sealed partial class BrowserSession : IAsyncDisposable public const string WidgetJs = """ (el) => { const a = (e, n) => (e.getAttribute(n) || '').toLowerCase(); + // Workday's search box — "How Did You Hear About Us?", Country Phone Code — says + // "Search" in its placeholder and is a question, never a site search (#453). + if (a(el, 'data-uxi-widget-type') === 'selectinput') return false; if (a(el, 'type') === 'search') return true; const own = [a(el, 'name'), a(el, 'id'), a(el, 'placeholder'), a(el, 'aria-label'), a(el, 'class')].join(' '); // "search" as its own word or suffix (keywordsearch, locationsearch, columnized-search), diff --git a/api/ApplyTrack.Api/Agent/Browser/BrowserSubmitter.cs b/api/ApplyTrack.Api/Agent/Browser/BrowserSubmitter.cs index 65b8fe5..3e93399 100644 --- a/api/ApplyTrack.Api/Agent/Browser/BrowserSubmitter.cs +++ b/api/ApplyTrack.Api/Agent/Browser/BrowserSubmitter.cs @@ -397,6 +397,9 @@ private async Task RunOnPageAsync( // Flexhire's location notice opens once Location is typed (#437). Acknowledged, // the field gets one more go. else if (await BrowserSession.AcknowledgeNoticeAsync(page) && await FillAsync(form, q, answer)) mapped.Add(q.Id); + // Workday's My Information shows the account's email as text, with no box: an + // address already on the page is answered (#453). + else if (q.Id == "std:email" && (await BodyTextAsync(form)).Contains(answer, StringComparison.OrdinalIgnoreCase)) mapped.Add(q.Id); else if (q.Required) unmapped.Add(q.Id); } @@ -1009,7 +1012,10 @@ private static async Task FillSelectInputAsync(IFrame page, ILocator box, if ((await SelectInputChipsAsync(box)).Length > 0) return true; var want = answer.Trim(); if (want.Length == 0) return false; - await box.FillAsync(want, new() { Timeout = 5_000 }); + // Typed key by key: a value set in one go is one Workday's widget does not hear, and + // Enter then makes no chip of it (#453). + await box.ClickAsync(new() { Timeout = 5_000 }); + await box.PressSequentiallyAsync(want, new() { Delay = 15 }); await box.PressAsync("Enter"); for (var i = 0; i < 16; i++) { @@ -1506,6 +1512,8 @@ private static async Task CommittedAsync(ILocator control, string chosen) return el.getAttribute('placeholder') || ''; }; const out = []; const seen = new Set(); + // Keyed the way discovery keys a control — name, then id — so the packet and this sweep + // name one box alike: Workday's City is id address--city and name city (#453). const add = (key, label) => { // A required star at either end is decoration: "* Country" is Country (#222). label = (label || '').replace(/^\s*\*\s*/, '').replace(/\*\s*$/, '').replace(/\s+/g, ' ').trim(); @@ -1546,7 +1554,7 @@ private static async Task CommittedAsync(ILocator control, string chosen) }; const addFile = (root, input, label) => { if (!visible(root) || fileFilled(root)) return; - add(input.id || input.getAttribute('name'), label); + add(input.getAttribute('name') || input.id, label); }; // Greenhouse's shape: the required flag sits on the upload group, never on the // file input inside it — which is why the résumé's own "required" never reached @@ -1611,7 +1619,7 @@ private static async Task CommittedAsync(ILocator control, string chosen) const combo = el.getAttribute('role') === 'combobox' || el.getAttribute('aria-autocomplete') === 'list'; if (combo) { if (!visible(el) || comboFilled(el)) continue; - add(el.id || el.getAttribute('name'), labelFor(el)); continue; + add(el.getAttribute('name') || el.id, labelFor(el)); continue; } if (!el.id && !el.getAttribute('name')) { // react-select's hidden required input: the combobox beside it is the one to name. @@ -1654,7 +1662,7 @@ private static async Task CommittedAsync(ILocator control, string chosen) empty = (el.value || '').trim().length === 0 || (el.selectedIndex === 0 && /^\s*-*\s*(?:select|choose|please (?:select|choose)|pick)\b/i.test(el.options[0]?.text || '')); else empty = (el.value || '').trim().length === 0; - if (empty) add(el.id || el.getAttribute('name'), labelFor(el)); + if (empty) add(el.getAttribute('name') || el.id, labelFor(el)); } // Workday (#443): a required drop-down still on "Select One" — keyed by its name, as // discovery keys it — and a required search box with no chip chosen. From 4503fc3a0eba84d364497910abfe9dbd7c4ac805 Mon Sep 17 00:00:00 2001 From: "Aaron K. Clark" Date: Tue, 13 Oct 2026 06:41:31 -0500 Subject: [PATCH 2/2] chore: prepare v1.68.4 release Bump all five version files to 1.68.4 and add #453 to BACKLOG.md. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_015Xrsyx9xJYfnT2BzLMNFsB --- .env.production.example | 2 +- BACKLOG.md | 1 + api/ApplyTrack.Api/ApplyTrack.Api.csproj | 2 +- pyproject.toml | 2 +- src/applytrack/__init__.py | 2 +- uv.lock | 2 +- 6 files changed, 6 insertions(+), 5 deletions(-) diff --git a/.env.production.example b/.env.production.example index 4d700f2..4c02d03 100644 --- a/.env.production.example +++ b/.env.production.example @@ -1,7 +1,7 @@ # Production deployments intentionally have no working secret defaults. # Copy this file to .env.production, replace every CHANGE-ME value, keep the # result out of version control, and pin a released OSApplyTrack version. -OSAPPLYTRACK_VERSION=1.68.3 +OSAPPLYTRACK_VERSION=1.68.4 POSTGRES_USER=applytrack POSTGRES_PASSWORD=CHANGE-ME-use-a-long-random-password POSTGRES_DB=applytrack diff --git a/BACKLOG.md b/BACKLOG.md index 6027616..337c27e 100644 --- a/BACKLOG.md +++ b/BACKLOG.md @@ -179,6 +179,7 @@ review items and unsupported forms still need human attention. - [x] [#446 — Agent role cannot write board_accounts: every judging pass has died since 2026-10-10](https://github.com/CryptoJones/OSApplyTrack/issues/446) (`agent_role.sql` granted the agent INSERT/UPDATE on its own tables but not on `board_accounts`, which it writes when it takes a Sign in now, keeps a renewed session, or saves an account #277 just made; `RenewSessionsAsync` runs before the judging pass, so once greenhouse.io's session came due the pass died on every tick and the Ready reconciler, the verdicts and portal renewal all stopped — the last `agent_events` row was 2026-10-09. **1.68.1:** the grant is re-applied on boot, and `DbRoleTests` proves the three writes and that a delete is still refused.) - [x] [#448 — Account creator glances for Workday's Apply instead of waiting: finastra.wd3 gets "no Create Account form after Apply"](https://github.com/CryptoJones/OSApplyTrack/issues/448) (the first Workday run after #446, finastra.wd3 on 2026-10-13: `RegisterWorkdayAsync` pressed Apply and Apply Manually with a glance the instant the posting's HTML landed, and Workday draws Apply only once its script has run — ~8 s there — so nothing was pressed and no form came; step 1 of the flow is fetched after the flow page is drawn too, blank for ~7 s. **1.68.2:** each step of the way in is waited for — Apply, Apply Manually, then whichever of Create Account, Sign In or "Sign in with email" the tenant shows, with 30 s for the lazy step — and the fixture draws Apply and the form late so a glance fails the test.) - [x] [#450 — Workday tenants that open step 1 on Create Account: the saved password is typed into Create Account and the sign-in is judged refused (finastra.wd3)](https://github.com/CryptoJones/OSApplyTrack/issues/450) (after #448 the creator made, proved and saved a finastra.wd3 account, and the re-run was "refused": Finastra's step 1 opens on Create Account — the same email and password boxes as Sign In plus Verify New Password and a "Sign In" link — so the saved account was typed into Create Account and the link pressed as the sign-in button, flipping the form empty. **1.68.3:** the way in waits up to 30 s for whatever step 1 shows, turns a Create Account over to Sign In through its link before any typing, and the same guard runs where the walk's own sign-ins start; a fixture variant that opens on Create Account, drawn late, signs in through the link and reaches Review.) +- [x] [#453 — Workday My Information hands back the same five fields every round (finastra.wd3)](https://github.com/CryptoJones/OSApplyTrack/issues/453) (on 1.68.3 the run signed in and filled the page, then handed back résumé, email, phone type, city and "How Did You Hear About Us?" on every round. Four causes, replayed from makemake: Workday's search box says "Search" in its placeholder and discovery read it as a site search and dropped it, while the sweep named it anyway; the sweep keyed a box by id and discovery by name, so City was unmapped under `address--city` and never drafted under `city`; Phone Device Type was discovered before its menu was read and the phone rule drafted the number; the email is drawn as text with no box. **1.68.4:** a Workday search box is a question and is typed into key by key before Enter; the sweep keys name then id as discovery does; an email already shown on the page is answered; the drafter answers the kind of phone (Mobile), where the posting was seen (LinkedIn, or the job-board option) instead of leaving it to the person, and the city and state of the résumé's location as their own boxes, with US abbreviations expanded for a fixed list.) - [x] [#417 — Agent: a résumé dropzone whose wrapping label is only "Click or drag & drop PDF" is never recognised as the résumé (Ethos) — fixed in 1.67.0: dropzone text never names a file field; its row's title does](https://github.com/CryptoJones/OSApplyTrack/issues/417) - [x] [#418 — Agent: an application form folded behind "Easy Apply" is never revealed (N-iX) — fixed in 1.67.0: "Easy/Quick/One-click Apply" open the form (N-iX itself then ends at a reCAPTCHA v2 handoff)](https://github.com/CryptoJones/OSApplyTrack/issues/418) - [x] [#419 — Agent: a footer newsletter signup (mc4wp) is discovered as an application question (Tecbrains) — fixed in 1.67.0: newsletter forms are page widgets](https://github.com/CryptoJones/OSApplyTrack/issues/419) diff --git a/api/ApplyTrack.Api/ApplyTrack.Api.csproj b/api/ApplyTrack.Api/ApplyTrack.Api.csproj index 0488731..b3212a5 100644 --- a/api/ApplyTrack.Api/ApplyTrack.Api.csproj +++ b/api/ApplyTrack.Api/ApplyTrack.Api.csproj @@ -5,7 +5,7 @@ enable enable ApplyTrack.Api - 1.68.3 + 1.68.4 Aaron K. Clark Copyright 2026 Aaron K. Clark Apache-2.0 diff --git a/pyproject.toml b/pyproject.toml index 3a6fe17..20d3a8f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "applytrack-poller" -version = "1.68.3" +version = "1.68.4" description = "Discovery poller for OSApplyTrack — fetches and scores remote job leads into shared Postgres." requires-python = ">=3.10" license = { text = "Apache-2.0" } diff --git a/src/applytrack/__init__.py b/src/applytrack/__init__.py index 2f00b1f..d0c591f 100644 --- a/src/applytrack/__init__.py +++ b/src/applytrack/__init__.py @@ -2,4 +2,4 @@ # Copyright 2026 Aaron K. Clark """applytrack — a local, markdown-backed job-application tracker.""" -__version__ = "1.68.3" +__version__ = "1.68.4" diff --git a/uv.lock b/uv.lock index 3fbe879..4d0affa 100644 --- a/uv.lock +++ b/uv.lock @@ -43,7 +43,7 @@ wheels = [ [[package]] name = "applytrack-poller" -version = "1.68.3" +version = "1.68.4" source = { editable = "." } dependencies = [ { name = "cryptography" },