atsfill 0.0.4.dev89__tar.gz → 0.0.4.dev91__tar.gz

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 (46) hide show
  1. {atsfill-0.0.4.dev89 → atsfill-0.0.4.dev91}/PKG-INFO +5 -5
  2. {atsfill-0.0.4.dev89 → atsfill-0.0.4.dev91}/README.md +4 -4
  3. {atsfill-0.0.4.dev89 → atsfill-0.0.4.dev91}/pyproject.toml +1 -1
  4. {atsfill-0.0.4.dev89 → atsfill-0.0.4.dev91}/pyproject.toml.orig +1 -1
  5. {atsfill-0.0.4.dev89 → atsfill-0.0.4.dev91}/src/atsfill/adapters/phenom.py +1 -1
  6. {atsfill-0.0.4.dev89 → atsfill-0.0.4.dev91}/src/atsfill/adapters/workday.py +23 -9
  7. {atsfill-0.0.4.dev89 → atsfill-0.0.4.dev91}/src/atsfill/adapters/wttj.py +85 -8
  8. {atsfill-0.0.4.dev89 → atsfill-0.0.4.dev91}/src/atsfill/cli.py +2 -0
  9. {atsfill-0.0.4.dev89 → atsfill-0.0.4.dev91}/src/atsfill/core/accounts.py +4 -2
  10. {atsfill-0.0.4.dev89 → atsfill-0.0.4.dev91}/src/atsfill/core/adapter.py +3 -1
  11. {atsfill-0.0.4.dev89 → atsfill-0.0.4.dev91}/src/atsfill/core/browser.py +1 -1
  12. {atsfill-0.0.4.dev89 → atsfill-0.0.4.dev91}/src/atsfill/core/commands.py +21 -7
  13. {atsfill-0.0.4.dev89 → atsfill-0.0.4.dev91}/src/atsfill/core/results.py +2 -2
  14. {atsfill-0.0.4.dev89 → atsfill-0.0.4.dev91}/src/atsfill/registry.toml +4 -3
  15. {atsfill-0.0.4.dev89 → atsfill-0.0.4.dev91}/LICENSE +0 -0
  16. {atsfill-0.0.4.dev89 → atsfill-0.0.4.dev91}/src/atsfill/__init__.py +0 -0
  17. {atsfill-0.0.4.dev89 → atsfill-0.0.4.dev91}/src/atsfill/adapters/__init__.py +0 -0
  18. {atsfill-0.0.4.dev89 → atsfill-0.0.4.dev91}/src/atsfill/adapters/ashby.py +0 -0
  19. {atsfill-0.0.4.dev89 → atsfill-0.0.4.dev91}/src/atsfill/adapters/bamboohr.py +0 -0
  20. {atsfill-0.0.4.dev89 → atsfill-0.0.4.dev91}/src/atsfill/adapters/breezy.py +0 -0
  21. {atsfill-0.0.4.dev89 → atsfill-0.0.4.dev91}/src/atsfill/adapters/dayforce.py +0 -0
  22. {atsfill-0.0.4.dev89 → atsfill-0.0.4.dev91}/src/atsfill/adapters/eightfold.py +0 -0
  23. {atsfill-0.0.4.dev89 → atsfill-0.0.4.dev91}/src/atsfill/adapters/greenhouse.py +0 -0
  24. {atsfill-0.0.4.dev89 → atsfill-0.0.4.dev91}/src/atsfill/adapters/hireology.py +0 -0
  25. {atsfill-0.0.4.dev89 → atsfill-0.0.4.dev91}/src/atsfill/adapters/hiringthing.py +0 -0
  26. {atsfill-0.0.4.dev89 → atsfill-0.0.4.dev91}/src/atsfill/adapters/jazzhr.py +0 -0
  27. {atsfill-0.0.4.dev89 → atsfill-0.0.4.dev91}/src/atsfill/adapters/lever.py +0 -0
  28. {atsfill-0.0.4.dev89 → atsfill-0.0.4.dev91}/src/atsfill/adapters/manatal.py +0 -0
  29. {atsfill-0.0.4.dev89 → atsfill-0.0.4.dev91}/src/atsfill/adapters/oracle.py +0 -0
  30. {atsfill-0.0.4.dev89 → atsfill-0.0.4.dev91}/src/atsfill/adapters/paycor.py +0 -0
  31. {atsfill-0.0.4.dev89 → atsfill-0.0.4.dev91}/src/atsfill/adapters/paylocity.py +0 -0
  32. {atsfill-0.0.4.dev89 → atsfill-0.0.4.dev91}/src/atsfill/adapters/personio.py +0 -0
  33. {atsfill-0.0.4.dev89 → atsfill-0.0.4.dev91}/src/atsfill/adapters/pinpoint.py +0 -0
  34. {atsfill-0.0.4.dev89 → atsfill-0.0.4.dev91}/src/atsfill/adapters/recruitee.py +0 -0
  35. {atsfill-0.0.4.dev89 → atsfill-0.0.4.dev91}/src/atsfill/adapters/rippling.py +0 -0
  36. {atsfill-0.0.4.dev89 → atsfill-0.0.4.dev91}/src/atsfill/adapters/smartrecruiters.py +0 -0
  37. {atsfill-0.0.4.dev89 → atsfill-0.0.4.dev91}/src/atsfill/adapters/teamtailor.py +0 -0
  38. {atsfill-0.0.4.dev89 → atsfill-0.0.4.dev91}/src/atsfill/adapters/workable.py +0 -0
  39. {atsfill-0.0.4.dev89 → atsfill-0.0.4.dev91}/src/atsfill/adapters/zohorecruit.py +0 -0
  40. {atsfill-0.0.4.dev89 → atsfill-0.0.4.dev91}/src/atsfill/core/__init__.py +0 -0
  41. {atsfill-0.0.4.dev89 → atsfill-0.0.4.dev91}/src/atsfill/core/attempts.py +0 -0
  42. {atsfill-0.0.4.dev89 → atsfill-0.0.4.dev91}/src/atsfill/core/matching.py +0 -0
  43. {atsfill-0.0.4.dev89 → atsfill-0.0.4.dev91}/src/atsfill/core/passwords.py +0 -0
  44. {atsfill-0.0.4.dev89 → atsfill-0.0.4.dev91}/src/atsfill/core/paths.py +0 -0
  45. {atsfill-0.0.4.dev89 → atsfill-0.0.4.dev91}/src/atsfill/core/registry.py +0 -0
  46. {atsfill-0.0.4.dev89 → atsfill-0.0.4.dev91}/src/atsfill/py.typed +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: atsfill
3
- Version: 0.0.4.dev89
3
+ Version: 0.0.4.dev91
4
4
  Summary: Read, dry-run and submit job application forms on hiring systems: Ashby, Greenhouse, Teamtailor, Welcome to the Jungle.
5
5
  Keywords: job-applications,ats,ashby,greenhouse,teamtailor,ai-agents,cli
6
6
  Author: shbernal
@@ -55,7 +55,7 @@ It is built for agents with a shell: every command prints one JSON document and
55
55
  ## Supported hiring systems
56
56
 
57
57
  <!-- badges -->
58
- ![oracle: dry run verified 2026-10-20](https://img.shields.io/badge/oracle-dry%20run%20verified%202026--10--20-7aa2f7) ![smartrecruiters: dry run verified 2026-10-20](https://img.shields.io/badge/smartrecruiters-dry%20run%20verified%202026--10--20-7aa2f7) ![greenhouse: dry run verified 2026-10-19](https://img.shields.io/badge/greenhouse-dry%20run%20verified%202026--10--19-7aa2f7) ![manatal: dry run verified 2026-10-20](https://img.shields.io/badge/manatal-dry%20run%20verified%202026--10--20-7aa2f7) ![paylocity: dry run verified 2026-10-20](https://img.shields.io/badge/paylocity-dry%20run%20verified%202026--10--20-7aa2f7) ![dayforce: dry run verified 2026-10-20](https://img.shields.io/badge/dayforce-dry%20run%20verified%202026--10--20-7aa2f7) ![workable: dry run verified 2026-10-20](https://img.shields.io/badge/workable-dry%20run%20verified%202026--10--20-7aa2f7) ![ashby: send verified 2026-10-19](https://img.shields.io/badge/ashby-send%20verified%202026--10--19-9ece6a) ![bamboohr: dry run verified 2026-10-21](https://img.shields.io/badge/bamboohr-dry%20run%20verified%202026--10--21-7aa2f7) ![teamtailor: send verified 2026-10-20](https://img.shields.io/badge/teamtailor-send%20verified%202026--10--20-9ece6a) ![jazzhr: dry run verified 2026-10-20](https://img.shields.io/badge/jazzhr-dry%20run%20verified%202026--10--20-7aa2f7) ![lever: dry run verified 2026-10-20](https://img.shields.io/badge/lever-dry%20run%20verified%202026--10--20-7aa2f7) ![hireology: dry run verified 2026-10-20](https://img.shields.io/badge/hireology-dry%20run%20verified%202026--10--20-7aa2f7) ![paycor: dry run verified 2026-10-20](https://img.shields.io/badge/paycor-dry%20run%20verified%202026--10--20-7aa2f7) ![zohorecruit: dry run verified 2026-10-22](https://img.shields.io/badge/zohorecruit-dry%20run%20verified%202026--10--22-7aa2f7) ![phenom: dry run verified 2026-10-21](https://img.shields.io/badge/phenom-dry%20run%20verified%202026--10--21-7aa2f7) ![breezy: dry run verified 2026-10-21](https://img.shields.io/badge/breezy-dry%20run%20verified%202026--10--21-7aa2f7) ![recruitee: dry run verified 2026-10-21](https://img.shields.io/badge/recruitee-dry%20run%20verified%202026--10--21-7aa2f7) ![rippling: dry run verified 2026-10-22](https://img.shields.io/badge/rippling-dry%20run%20verified%202026--10--22-7aa2f7) ![personio: dry run verified 2026-10-22](https://img.shields.io/badge/personio-dry%20run%20verified%202026--10--22-7aa2f7) ![eightfold: dry run verified 2026-10-22](https://img.shields.io/badge/eightfold-dry%20run%20verified%202026--10--22-7aa2f7) ![pinpoint: dry run verified 2026-10-22](https://img.shields.io/badge/pinpoint-dry%20run%20verified%202026--10--22-7aa2f7) ![hiringthing: dry run verified 2026-10-22](https://img.shields.io/badge/hiringthing-dry%20run%20verified%202026--10--22-7aa2f7) ![wttj: send verified 2026-10-20](https://img.shields.io/badge/wttj-send%20verified%202026--10--20-9ece6a) ![workday: mapped 2026-10-20](https://img.shields.io/badge/workday-mapped%202026--10--20-a9b1d6)
58
+ ![oracle: dry run verified 2026-10-20](https://img.shields.io/badge/oracle-dry%20run%20verified%202026--10--20-7aa2f7) ![smartrecruiters: dry run verified 2026-10-20](https://img.shields.io/badge/smartrecruiters-dry%20run%20verified%202026--10--20-7aa2f7) ![greenhouse: dry run verified 2026-10-19](https://img.shields.io/badge/greenhouse-dry%20run%20verified%202026--10--19-7aa2f7) ![manatal: dry run verified 2026-10-20](https://img.shields.io/badge/manatal-dry%20run%20verified%202026--10--20-7aa2f7) ![paylocity: dry run verified 2026-10-20](https://img.shields.io/badge/paylocity-dry%20run%20verified%202026--10--20-7aa2f7) ![dayforce: dry run verified 2026-10-20](https://img.shields.io/badge/dayforce-dry%20run%20verified%202026--10--20-7aa2f7) ![workable: dry run verified 2026-10-20](https://img.shields.io/badge/workable-dry%20run%20verified%202026--10--20-7aa2f7) ![ashby: send verified 2026-10-19](https://img.shields.io/badge/ashby-send%20verified%202026--10--19-9ece6a) ![bamboohr: dry run verified 2026-10-21](https://img.shields.io/badge/bamboohr-dry%20run%20verified%202026--10--21-7aa2f7) ![teamtailor: send verified 2026-10-20](https://img.shields.io/badge/teamtailor-send%20verified%202026--10--20-9ece6a) ![jazzhr: dry run verified 2026-10-20](https://img.shields.io/badge/jazzhr-dry%20run%20verified%202026--10--20-7aa2f7) ![lever: dry run verified 2026-10-20](https://img.shields.io/badge/lever-dry%20run%20verified%202026--10--20-7aa2f7) ![hireology: dry run verified 2026-10-20](https://img.shields.io/badge/hireology-dry%20run%20verified%202026--10--20-7aa2f7) ![paycor: dry run verified 2026-10-20](https://img.shields.io/badge/paycor-dry%20run%20verified%202026--10--20-7aa2f7) ![zohorecruit: dry run verified 2026-10-22](https://img.shields.io/badge/zohorecruit-dry%20run%20verified%202026--10--22-7aa2f7) ![phenom: dry run verified 2026-10-21](https://img.shields.io/badge/phenom-dry%20run%20verified%202026--10--21-7aa2f7) ![breezy: dry run verified 2026-10-21](https://img.shields.io/badge/breezy-dry%20run%20verified%202026--10--21-7aa2f7) ![recruitee: dry run verified 2026-10-21](https://img.shields.io/badge/recruitee-dry%20run%20verified%202026--10--21-7aa2f7) ![rippling: dry run verified 2026-10-22](https://img.shields.io/badge/rippling-dry%20run%20verified%202026--10--22-7aa2f7) ![personio: dry run verified 2026-10-22](https://img.shields.io/badge/personio-dry%20run%20verified%202026--10--22-7aa2f7) ![eightfold: dry run verified 2026-10-22](https://img.shields.io/badge/eightfold-dry%20run%20verified%202026--10--22-7aa2f7) ![pinpoint: dry run verified 2026-10-22](https://img.shields.io/badge/pinpoint-dry%20run%20verified%202026--10--22-7aa2f7) ![hiringthing: dry run verified 2026-10-22](https://img.shields.io/badge/hiringthing-dry%20run%20verified%202026--10--22-7aa2f7) ![wttj: send verified 2026-10-20](https://img.shields.io/badge/wttj-send%20verified%202026--10--20-9ece6a) ![workday: dry run verified 2026-10-24](https://img.shields.io/badge/workday-dry%20run%20verified%202026--10--24-7aa2f7)
59
59
  <!-- /badges -->
60
60
 
61
61
  | System | Account needed | `read` without a browser |
@@ -65,7 +65,7 @@ It is built for agents with a shell: every command prints one JSON document and
65
65
  | Teamtailor | no | no |
66
66
  | Welcome to the Jungle | yes, `atsfill login wttj` | no |
67
67
 
68
- Any other system returns `unsupported`, so the caller can fall back to driving a browser itself.
68
+ Any other system returns `unsupported`, so the consumer can fall back to driving a browser itself.
69
69
 
70
70
  ## Why atsfill?
71
71
 
@@ -133,9 +133,9 @@ A dry run is not invisible: some systems save answers or the resume as they are
133
133
  `atsfill status` lists each system's side effects, whether it is allowed to submit, and how far atsfill has verified it: [the systems](docs/adapters/README.md).
134
134
  A dry run or a `send` through an adapter no send was verified on carries a `warning`; the send goes ahead anyway.
135
135
 
136
- One home can apply for more than one person: `--user <name>` gives each their own sessions, double-send guard and `[users.<name>]` section in the config file ([ADR 0015](docs/adr/0015-users-and-accounts.md)).
136
+ One home can send for more than one user: `--user <name>` gives each their own sessions, double-send guard and `[users.<name>]` section in the config file ([ADR 0015](docs/adr/0015-users-and-accounts.md)).
137
137
  Given an account's email (`atsfill login <adapter> --email`) and a password source (a `[password-commands]` entry such as `rbw get example.com {email}`, or `--password -` on stdin), atsfill signs in by itself, and again whenever a form command finds the session signed out.
138
- Where a system's sign-up allows it, `atsfill login <adapter> --create --email <address>` creates the account with a password atsfill generates and keeps in the system's secret store, never printed; creating an account is a side effect, never part of `fill` or `send`.
138
+ Where a system's sign-up allows it, `atsfill login <adapter> --create --email <address>` (with `--name` where the sign-up asks the user's name) creates the account with a password atsfill generates and keeps in the system's secret store, never printed; creating an account is a side effect, never part of `fill` or `send`.
139
139
 
140
140
  ## How it works
141
141
 
@@ -29,7 +29,7 @@ It is built for agents with a shell: every command prints one JSON document and
29
29
  ## Supported hiring systems
30
30
 
31
31
  <!-- badges -->
32
- ![oracle: dry run verified 2026-10-20](https://img.shields.io/badge/oracle-dry%20run%20verified%202026--10--20-7aa2f7) ![smartrecruiters: dry run verified 2026-10-20](https://img.shields.io/badge/smartrecruiters-dry%20run%20verified%202026--10--20-7aa2f7) ![greenhouse: dry run verified 2026-10-19](https://img.shields.io/badge/greenhouse-dry%20run%20verified%202026--10--19-7aa2f7) ![manatal: dry run verified 2026-10-20](https://img.shields.io/badge/manatal-dry%20run%20verified%202026--10--20-7aa2f7) ![paylocity: dry run verified 2026-10-20](https://img.shields.io/badge/paylocity-dry%20run%20verified%202026--10--20-7aa2f7) ![dayforce: dry run verified 2026-10-20](https://img.shields.io/badge/dayforce-dry%20run%20verified%202026--10--20-7aa2f7) ![workable: dry run verified 2026-10-20](https://img.shields.io/badge/workable-dry%20run%20verified%202026--10--20-7aa2f7) ![ashby: send verified 2026-10-19](https://img.shields.io/badge/ashby-send%20verified%202026--10--19-9ece6a) ![bamboohr: dry run verified 2026-10-21](https://img.shields.io/badge/bamboohr-dry%20run%20verified%202026--10--21-7aa2f7) ![teamtailor: send verified 2026-10-20](https://img.shields.io/badge/teamtailor-send%20verified%202026--10--20-9ece6a) ![jazzhr: dry run verified 2026-10-20](https://img.shields.io/badge/jazzhr-dry%20run%20verified%202026--10--20-7aa2f7) ![lever: dry run verified 2026-10-20](https://img.shields.io/badge/lever-dry%20run%20verified%202026--10--20-7aa2f7) ![hireology: dry run verified 2026-10-20](https://img.shields.io/badge/hireology-dry%20run%20verified%202026--10--20-7aa2f7) ![paycor: dry run verified 2026-10-20](https://img.shields.io/badge/paycor-dry%20run%20verified%202026--10--20-7aa2f7) ![zohorecruit: dry run verified 2026-10-22](https://img.shields.io/badge/zohorecruit-dry%20run%20verified%202026--10--22-7aa2f7) ![phenom: dry run verified 2026-10-21](https://img.shields.io/badge/phenom-dry%20run%20verified%202026--10--21-7aa2f7) ![breezy: dry run verified 2026-10-21](https://img.shields.io/badge/breezy-dry%20run%20verified%202026--10--21-7aa2f7) ![recruitee: dry run verified 2026-10-21](https://img.shields.io/badge/recruitee-dry%20run%20verified%202026--10--21-7aa2f7) ![rippling: dry run verified 2026-10-22](https://img.shields.io/badge/rippling-dry%20run%20verified%202026--10--22-7aa2f7) ![personio: dry run verified 2026-10-22](https://img.shields.io/badge/personio-dry%20run%20verified%202026--10--22-7aa2f7) ![eightfold: dry run verified 2026-10-22](https://img.shields.io/badge/eightfold-dry%20run%20verified%202026--10--22-7aa2f7) ![pinpoint: dry run verified 2026-10-22](https://img.shields.io/badge/pinpoint-dry%20run%20verified%202026--10--22-7aa2f7) ![hiringthing: dry run verified 2026-10-22](https://img.shields.io/badge/hiringthing-dry%20run%20verified%202026--10--22-7aa2f7) ![wttj: send verified 2026-10-20](https://img.shields.io/badge/wttj-send%20verified%202026--10--20-9ece6a) ![workday: mapped 2026-10-20](https://img.shields.io/badge/workday-mapped%202026--10--20-a9b1d6)
32
+ ![oracle: dry run verified 2026-10-20](https://img.shields.io/badge/oracle-dry%20run%20verified%202026--10--20-7aa2f7) ![smartrecruiters: dry run verified 2026-10-20](https://img.shields.io/badge/smartrecruiters-dry%20run%20verified%202026--10--20-7aa2f7) ![greenhouse: dry run verified 2026-10-19](https://img.shields.io/badge/greenhouse-dry%20run%20verified%202026--10--19-7aa2f7) ![manatal: dry run verified 2026-10-20](https://img.shields.io/badge/manatal-dry%20run%20verified%202026--10--20-7aa2f7) ![paylocity: dry run verified 2026-10-20](https://img.shields.io/badge/paylocity-dry%20run%20verified%202026--10--20-7aa2f7) ![dayforce: dry run verified 2026-10-20](https://img.shields.io/badge/dayforce-dry%20run%20verified%202026--10--20-7aa2f7) ![workable: dry run verified 2026-10-20](https://img.shields.io/badge/workable-dry%20run%20verified%202026--10--20-7aa2f7) ![ashby: send verified 2026-10-19](https://img.shields.io/badge/ashby-send%20verified%202026--10--19-9ece6a) ![bamboohr: dry run verified 2026-10-21](https://img.shields.io/badge/bamboohr-dry%20run%20verified%202026--10--21-7aa2f7) ![teamtailor: send verified 2026-10-20](https://img.shields.io/badge/teamtailor-send%20verified%202026--10--20-9ece6a) ![jazzhr: dry run verified 2026-10-20](https://img.shields.io/badge/jazzhr-dry%20run%20verified%202026--10--20-7aa2f7) ![lever: dry run verified 2026-10-20](https://img.shields.io/badge/lever-dry%20run%20verified%202026--10--20-7aa2f7) ![hireology: dry run verified 2026-10-20](https://img.shields.io/badge/hireology-dry%20run%20verified%202026--10--20-7aa2f7) ![paycor: dry run verified 2026-10-20](https://img.shields.io/badge/paycor-dry%20run%20verified%202026--10--20-7aa2f7) ![zohorecruit: dry run verified 2026-10-22](https://img.shields.io/badge/zohorecruit-dry%20run%20verified%202026--10--22-7aa2f7) ![phenom: dry run verified 2026-10-21](https://img.shields.io/badge/phenom-dry%20run%20verified%202026--10--21-7aa2f7) ![breezy: dry run verified 2026-10-21](https://img.shields.io/badge/breezy-dry%20run%20verified%202026--10--21-7aa2f7) ![recruitee: dry run verified 2026-10-21](https://img.shields.io/badge/recruitee-dry%20run%20verified%202026--10--21-7aa2f7) ![rippling: dry run verified 2026-10-22](https://img.shields.io/badge/rippling-dry%20run%20verified%202026--10--22-7aa2f7) ![personio: dry run verified 2026-10-22](https://img.shields.io/badge/personio-dry%20run%20verified%202026--10--22-7aa2f7) ![eightfold: dry run verified 2026-10-22](https://img.shields.io/badge/eightfold-dry%20run%20verified%202026--10--22-7aa2f7) ![pinpoint: dry run verified 2026-10-22](https://img.shields.io/badge/pinpoint-dry%20run%20verified%202026--10--22-7aa2f7) ![hiringthing: dry run verified 2026-10-22](https://img.shields.io/badge/hiringthing-dry%20run%20verified%202026--10--22-7aa2f7) ![wttj: send verified 2026-10-20](https://img.shields.io/badge/wttj-send%20verified%202026--10--20-9ece6a) ![workday: dry run verified 2026-10-24](https://img.shields.io/badge/workday-dry%20run%20verified%202026--10--24-7aa2f7)
33
33
  <!-- /badges -->
34
34
 
35
35
  | System | Account needed | `read` without a browser |
@@ -39,7 +39,7 @@ It is built for agents with a shell: every command prints one JSON document and
39
39
  | Teamtailor | no | no |
40
40
  | Welcome to the Jungle | yes, `atsfill login wttj` | no |
41
41
 
42
- Any other system returns `unsupported`, so the caller can fall back to driving a browser itself.
42
+ Any other system returns `unsupported`, so the consumer can fall back to driving a browser itself.
43
43
 
44
44
  ## Why atsfill?
45
45
 
@@ -107,9 +107,9 @@ A dry run is not invisible: some systems save answers or the resume as they are
107
107
  `atsfill status` lists each system's side effects, whether it is allowed to submit, and how far atsfill has verified it: [the systems](docs/adapters/README.md).
108
108
  A dry run or a `send` through an adapter no send was verified on carries a `warning`; the send goes ahead anyway.
109
109
 
110
- One home can apply for more than one person: `--user <name>` gives each their own sessions, double-send guard and `[users.<name>]` section in the config file ([ADR 0015](docs/adr/0015-users-and-accounts.md)).
110
+ One home can send for more than one user: `--user <name>` gives each their own sessions, double-send guard and `[users.<name>]` section in the config file ([ADR 0015](docs/adr/0015-users-and-accounts.md)).
111
111
  Given an account's email (`atsfill login <adapter> --email`) and a password source (a `[password-commands]` entry such as `rbw get example.com {email}`, or `--password -` on stdin), atsfill signs in by itself, and again whenever a form command finds the session signed out.
112
- Where a system's sign-up allows it, `atsfill login <adapter> --create --email <address>` creates the account with a password atsfill generates and keeps in the system's secret store, never printed; creating an account is a side effect, never part of `fill` or `send`.
112
+ Where a system's sign-up allows it, `atsfill login <adapter> --create --email <address>` (with `--name` where the sign-up asks the user's name) creates the account with a password atsfill generates and keeps in the system's secret store, never printed; creating an account is a side effect, never part of `fill` or `send`.
113
113
 
114
114
  ## How it works
115
115
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "atsfill"
3
- version = "0.0.4.dev89"
3
+ version = "0.0.4.dev91"
4
4
  description = "Read, dry-run and submit job application forms on hiring systems: Ashby, Greenhouse, Teamtailor, Welcome to the Jungle."
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "atsfill"
3
- version = "0.0.4.dev89"
3
+ version = "0.0.4.dev91"
4
4
  description = "Read, dry-run and submit job application forms on hiring systems: Ashby, Greenhouse, Teamtailor, Welcome to the Jungle."
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -236,7 +236,7 @@ class Phenom:
236
236
  SideEffect(
237
237
  "fill",
238
238
  "hiring_system",
239
- "each step is submitted to Phenom on Next, which keeps a candidate profile from the"
239
+ "each step is submitted to Phenom on Next, which keeps an account profile from the"
240
240
  " first step",
241
241
  ),
242
242
  SideEffect("submit", "employer", "the application reaches the employer"),
@@ -56,6 +56,7 @@ PICKED = "[data-automation-id=selectedItem]"
56
56
  DELETE_PICK = "[data-automation-id=DELETE_charm]"
57
57
  SEARCH = "input[data-uxi-widget-type=selectinput]"
58
58
  BACK = "[data-automation-id=backButton]:visible"
59
+ ADDRESS = "address--"
59
60
  PHONE_CODE_ID = "phoneNumber--countryPhoneCode"
60
61
  PHONE_CODE = f'[data-fkit-id="{PHONE_CODE_ID}"]'
61
62
  PHONE_CODES = "/values/phone/countryCodes"
@@ -178,6 +179,7 @@ def _profile_hosts(request: Request, scope: str | None) -> list[str]:
178
179
  class Workday:
179
180
  name = "workday"
180
181
  per_employer = True
182
+ asks_name = False
181
183
  known_gaps = {
182
184
  "experience_entries": Gap.FLOW,
183
185
  "long_wizard": Gap.FLOW,
@@ -234,7 +236,7 @@ class Workday:
234
236
  _email_form(page)
235
237
  _submit(page, "signInSubmitButton", email, password)
236
238
 
237
- def sign_up(self, page: Page, email: str, password: str) -> None:
239
+ def sign_up(self, page: Page, email: str, password: str, name: str | None) -> None:
238
240
  _email_form(page)
239
241
  page.locator("[data-automation-id=createAccountLink]").click()
240
242
  page.locator("[data-automation-id=verifyPassword]").fill(password)
@@ -315,7 +317,7 @@ class Workday:
315
317
  _until_rendered(page, pending)
316
318
  page.wait_for_timeout(SETTLE_MS)
317
319
  self._unread(page)
318
- self._values |= {r["id"]: r["value"] for r in _page_fields(page)}
320
+ self._values |= {r["id"]: r["value"] for r in _filled(page, answers)}
319
321
  if self._values.get(PHONE_NUMBER) and (code := _phone_code(page)):
320
322
  self._values[PHONE_NUMBER] = f"+{code} {self._values[PHONE_NUMBER]}"
321
323
  _save_and_continue(page)
@@ -323,16 +325,17 @@ class Workday:
323
325
 
324
326
  def _unread(self, page: Page) -> None:
325
327
  """Stops on required fields the read did not list: on My Information, the address of a
326
- country other than the form's; on a later page, one the read could not reach."""
327
- unread = [
328
- r["label"] for r in _page_fields(page) if r["required"] and r["id"] not in self._fields
329
- ]
328
+ country other than the form's, or a follow-up an answer opened (a referral's name); on a
329
+ later page, one the read could not reach."""
330
+ unread = [r for r in _page_fields(page) if r["required"] and r["id"] not in self._fields]
330
331
  if not unread:
331
332
  return
332
- step, labels = _step(page), ", ".join(unread)
333
- if step == MY_INFORMATION:
333
+ step, labels = _step(page), ", ".join(r["label"] for r in unread)
334
+ if step == MY_INFORMATION and any(r["id"].startswith(ADDRESS) for r in unread):
334
335
  detail = f"the country answered shows fields the read did not: {labels}"
335
336
  raise Unsupported(known_gap(self, "other_country", detail))
337
+ if step == MY_INFORMATION:
338
+ raise NotTaken(f"{step} shows fields the read did not: {labels}; an answer opened them")
336
339
  raise NotTaken(f"{step} shows fields the read did not: {labels}; read again")
337
340
 
338
341
  def read_back(self, page: Page) -> dict[str, Answer]:
@@ -530,6 +533,17 @@ def _rendered(page: Page, answers: dict[str, Answer]) -> list[dict[str, Any]]:
530
533
  return raws
531
534
 
532
535
 
536
+ def _filled(page: Page, answers: dict[str, Answer]) -> list[dict[str, Any]]:
537
+ """The page's fields once each answered one shows a value: a picked item can leave the
538
+ page for a moment as it renders again."""
539
+ for _ in range(RENDER_TRIES):
540
+ raws = _page_fields(page)
541
+ if all(r["value"] not in (None, "", []) for r in raws if r["id"] in answers):
542
+ break
543
+ page.wait_for_timeout(SETTLE_MS)
544
+ return raws
545
+
546
+
533
547
  def _field(raw: dict[str, Any]) -> Field | None:
534
548
  kind, options = KINDS.get(raw["kind"]), raw["options"]
535
549
  if raw["kind"] == "box":
@@ -703,7 +717,7 @@ def _fill(page: Page, field: Field, raw: dict[str, Any], answer: Answer) -> None
703
717
  case "file", _:
704
718
  files = [answer] if isinstance(answer, str) else answer
705
719
  delete = box.locator(DELETE_FILE)
706
- while (count := delete.count()) > 0: # the draft's or the profile's files
720
+ while (count := delete.count()) > 0: # the draft's or the account profile's files
707
721
  delete.first.click()
708
722
  deadline = time.monotonic() + UPLOAD_TIMEOUT_MS / 1000
709
723
  while delete.count() == count and time.monotonic() < deadline:
@@ -1,8 +1,9 @@
1
1
  """Welcome to the Jungle (`welcometothejungle.com` jobs and their `easyapply.jobs` links), as
2
2
  docs/adapters/wttj.md observes.
3
3
 
4
- Applying needs an account: the form opens in a modal, prefilled from the profile. Nothing is
5
- sent before submit, so the read-back is the form's inputs and the CV's shown name.
4
+ Applying needs an account, which atsfill can sign in to and create: the form opens in a modal,
5
+ prefilled from the account profile. Nothing is sent before submit, so the read-back is the form's
6
+ inputs and the CV's shown name.
6
7
  """
7
8
 
8
9
  import re
@@ -11,14 +12,18 @@ from typing import Any
11
12
  from urllib.parse import urlsplit
12
13
 
13
14
  from atsfill.core import browser, matching
14
- from atsfill.core.adapter import Blocked, Gap, NotTaken, Unsupported
15
- from atsfill.core.browser import Locator, Page, Request, Timeout
15
+ from atsfill.core.adapter import Blocked, Gap, NotTaken, Refused, Unsupported
16
+ from atsfill.core.browser import Locator, Page, Request, Response, Timeout
16
17
  from atsfill.core.results import Answer, Field, FieldType, SideEffect
17
18
 
18
19
  HOST = "www.welcometothejungle.com"
19
20
  JOB = re.compile(r"^/(?:[a-z]{2}/)?companies/([^/]+)/jobs/([^/?#]+)")
20
21
  FORM = "form:has([data-testid=apply-form-submit])"
21
22
  ME = "https://api.welcometothejungle.com/api/v2/users/me"
23
+ SIGN_UP = f"https://{HOST}/en/authenticate/signup"
24
+ ACCOUNT_TIMEOUT_MS = 20_000
25
+ ACCOUNT_TIMEOUT_S = 20
26
+ ACCOUNT_POLL_MS = 250
22
27
  FORM_TIMEOUT_MS = 20_000
23
28
  SIGNED_IN_TIMEOUT_S = 10
24
29
  SUBMIT_TIMEOUT_MS = 90_000
@@ -58,8 +63,12 @@ _READ_BACK = """(form, ids) => Object.fromEntries(ids.map(id => {
58
63
  class WelcomeToTheJungle:
59
64
  name = "wttj"
60
65
  per_employer = False
66
+ asks_name = True # the profile's first name
61
67
  known_gaps: dict[str, Gap] = {}
62
- side_effects = [SideEffect("submit", "employer", "the application reaches the employer")]
68
+ side_effects = [
69
+ SideEffect("sign_up", "hiring_system", "login --create creates the account"),
70
+ SideEffect("submit", "employer", "the application reaches the employer"),
71
+ ]
63
72
 
64
73
  def __init__(self) -> None:
65
74
  self._fields: dict[str, Field] = {}
@@ -85,6 +94,17 @@ class WelcomeToTheJungle:
85
94
  """`users/me` answers 200 for an account, 204 for a visitor."""
86
95
  return request.get(ME).status == 200
87
96
 
97
+ def sign_in(self, page: Page, email: str, password: str) -> None:
98
+ _account_form(page, "sign-in-form", {"email": email, "password": password}, "/sessions")
99
+
100
+ def sign_up(self, page: Page, email: str, password: str, name: str | None) -> None:
101
+ """Creates the account, signed in, then waits for its confirmation link: clicking it
102
+ signs every session out."""
103
+ page.goto(SIGN_UP)
104
+ values = {"firstname": name or "", "email": email, "password": password}
105
+ _account_form(page, "sign-up-form", values, "/registrations")
106
+ raise Blocked("email_link: the sign-up sent a confirmation link")
107
+
88
108
  def read_form(self, page: Page) -> list[Field]:
89
109
  """Opens the apply modal; a job that applies on another site has no form here."""
90
110
  if not JOB.match(urlsplit(page.url).path):
@@ -118,7 +138,7 @@ class WelcomeToTheJungle:
118
138
  while location.count() and not location.input_value() and time.monotonic() < deadline:
119
139
  page.wait_for_timeout(
120
140
  LOCATION_POLL_MS
121
- ) # the profile's location comes last, a second later
141
+ ) # the account profile's location comes last, a second later
122
142
  fields: dict[str, Field] = {}
123
143
  for raw in form.evaluate(_READ):
124
144
  if raw["id"] not in fields and (field := _field(raw)) is not None:
@@ -135,7 +155,7 @@ class WelcomeToTheJungle:
135
155
  box = target.locator("xpath=..")
136
156
  remove = box.locator("button[title=Remove]")
137
157
  if remove.count():
138
- remove.click() # the profile's CV; never made the default
158
+ remove.click() # the account profile's CV; never made the default
139
159
  target.set_input_files(answer)
140
160
  box.locator(f'[data-id="{answer.rsplit("/", 1)[-1]}"]').wait_for()
141
161
  case FieldType.CHOICE, str():
@@ -168,6 +188,63 @@ class WelcomeToTheJungle:
168
188
  raise RuntimeError(f"POST {response.url} answered {response.status}")
169
189
 
170
190
 
191
+ def _account_form(page: Page, form: str, values: dict[str, str], posts: str) -> None:
192
+ """Fills the sign-in or sign-up form, submits it and returns once the system took it; raises
193
+ what the system answered, or what the form says under a field it refused without posting."""
194
+ try:
195
+ page.locator(f"[data-testid={form}-submit-button]").wait_for(timeout=ACCOUNT_TIMEOUT_MS)
196
+ except Timeout:
197
+ if "too many requests" in page.locator("body").inner_text().lower():
198
+ reason = f"rate_limited: {urlsplit(page.url).path} answers too many requests"
199
+ raise Blocked(reason) from None
200
+ raise Unsupported(f"unhandled_rendering: no {form} on {page.url}") from None
201
+ for field, value in values.items():
202
+ page.locator(f"[data-testid={form}-{field}-input]").fill(value)
203
+ # posted before the page has its CSRF token, the form is answered 403 and says nothing
204
+ page.wait_for_function(
205
+ "() => document.cookie.includes('csrf-token=')", timeout=ACCOUNT_TIMEOUT_MS
206
+ )
207
+ answered: list[Response] = []
208
+
209
+ def on_response(response: Response) -> None:
210
+ if response.request.method == "POST" and urlsplit(response.url).path.endswith(posts):
211
+ answered.append(response)
212
+
213
+ page.on("response", on_response)
214
+ on_form = page.url
215
+ page.locator(f"[data-testid={form}-submit-button]").click()
216
+ said = page.locator(f"[data-testid^={form}-][data-testid$=-error]")
217
+ deadline = time.monotonic() + ACCOUNT_TIMEOUT_S
218
+ try:
219
+ while time.monotonic() < deadline:
220
+ if answered:
221
+ _account_answer(answered[0])
222
+ # the page leaves the form on its own: a navigation before then is aborted
223
+ page.wait_for_url(lambda url: url != on_form, timeout=ACCOUNT_TIMEOUT_MS)
224
+ return
225
+ if said.count():
226
+ field = said.first.get_attribute("data-testid") or ""
227
+ text = said.first.inner_text().strip()
228
+ if field.endswith("-password-error"):
229
+ raise Refused(f"password_rule: {text}")
230
+ raise Unsupported(f"unhandled_rendering: the form says {text!r}")
231
+ page.wait_for_timeout(ACCOUNT_POLL_MS)
232
+ finally:
233
+ page.remove_listener("response", on_response)
234
+ raise Unsupported("unhandled_rendering: the form answered nothing")
235
+
236
+
237
+ def _account_answer(response: Response) -> None:
238
+ match response.status:
239
+ case 200 | 201:
240
+ return
241
+ case 401:
242
+ raise Refused("wrong_password")
243
+ case 422 if "email" in response.json().get("errors", {}):
244
+ raise Refused("email_in_use")
245
+ raise RuntimeError(f"POST {urlsplit(response.url).path} answered {response.status}")
246
+
247
+
171
248
  def _suggest(page: Page, field: Field, box: Locator, answer: str) -> None:
172
249
  """Retypes the answer key by key, which loads the suggestions, and picks the first that
173
250
  matches it, never "current position" (the item with an icon, which geolocates). Typed too
@@ -175,7 +252,7 @@ def _suggest(page: Page, field: Field, box: Locator, answer: str) -> None:
175
252
  options = page.locator("[role=option]:visible").filter(has_not=page.locator("svg"))
176
253
  for _ in range(SUGGEST_TRIES):
177
254
  box.click()
178
- box.press("Control+a") # `fill("")`, select-all and Delete, leaves the profile's value
255
+ box.press("Control+a") # `fill("")` (select-all, Delete) leaves the account profile's value
179
256
  box.press("Backspace")
180
257
  box.press_sequentially(answer, delay=KEY_DELAY_MS)
181
258
  deadline = time.monotonic() + SUGGEST_TIMEOUT_S
@@ -105,6 +105,7 @@ def parser() -> argparse.ArgumentParser:
105
105
  action="store_true",
106
106
  help="create the account with --email and a password atsfill generates and stores",
107
107
  )
108
+ login.add_argument("--name", help="the user's name, for a sign-up that asks it")
108
109
  for name in ("read", "fill", "send", "status", "login", "logout"):
109
110
  sub.choices[name].add_argument(
110
111
  "-v", "--verbose", action="store_true", help="the whole result, every key, indented"
@@ -170,6 +171,7 @@ def run(args: argparse.Namespace) -> int:
170
171
  password=password,
171
172
  window=args.window,
172
173
  create=args.create,
174
+ name=args.name,
173
175
  **where,
174
176
  )
175
177
  except ValueError as e:
@@ -54,14 +54,16 @@ def sign_in(
54
54
  password: str,
55
55
  session: Path,
56
56
  create: bool = False,
57
+ name: str | None = None,
57
58
  ) -> None:
58
- """Signs in, or creates the account, from the system's login page and stores the session.
59
+ """Signs in, or creates the account (with the user's `name` where it asks one), from the
60
+ system's login page and stores the session.
59
61
  Raises what the adapter raises, without the password in it."""
60
62
  page.goto(adapter.login_url(scope))
61
63
  try:
62
64
  if create:
63
65
  assert isinstance(adapter, CreatesAccount)
64
- adapter.sign_up(page, address, password)
66
+ adapter.sign_up(page, address, password, name)
65
67
  else:
66
68
  adapter.sign_in(page, address, password)
67
69
  except Exception as e:
@@ -128,7 +128,9 @@ class CreatesAccount(SignsIn, Protocol):
128
128
  """An adapter atsfill can create an account through, where the system's sign-up allows it.
129
129
  Account creation is a side effect it declares (ADR 0008)."""
130
130
 
131
- def sign_up(self, page: Page, email: str, password: str) -> None:
131
+ asks_name: bool # the sign-up asks the user's name
132
+
133
+ def sign_up(self, page: Page, email: str, password: str, name: str | None) -> None:
132
134
  """Creates the account from the page `login_url` loaded, signed in once it returns.
133
135
  Raises as `sign_in` does; `Refused` names the rule a `password_rule` page showed."""
134
136
  ...
@@ -124,7 +124,7 @@ def _build_blocker() -> None:
124
124
  local = f"assets/{BLOCKER_LISTS}/{key}.txt"
125
125
  (build / local).parent.mkdir(exist_ok=True)
126
126
  (build / local).write_text(_filter_list([*urls, *entry.get("cdnURLs", [])]))
127
- entry.pop("off", None) # on from the start, the way a fresh profile reads it
127
+ entry.pop("off", None) # on from the start, the way a fresh browser profile reads it
128
128
  entry["contentURL"] = [local, *urls]
129
129
  entries["atsfill"] = {"content": "filters", "group": "custom", "title": "atsfill",
130
130
  "contentURL": [FILTERS]} # fmt: skip
@@ -425,13 +425,16 @@ def login(
425
425
  password: str | None = None,
426
426
  window: bool = False,
427
427
  create: bool = False,
428
+ name: str | None = None,
428
429
  data_dir: Path | None = None,
429
430
  ) -> SessionReport:
430
431
  """Signs in with the account's password source, headless, and stores the session; without
431
432
  a source, or with `window`, opens a window on the system's login page for a person.
432
433
 
433
434
  `email` is remembered for the account; `password` is used for this sign-in only. `create`
434
- creates the account with a password atsfill generates and stores first (a side effect)."""
435
+ creates the account with a password atsfill generates and stores first (a side effect),
436
+ giving `name` where the sign-up asks it; with a password already stored, the account may
437
+ exist, so it signs in with that password and keeps the account's email instead."""
435
438
  account, data = _account(adapter), paths.data_dir(data_dir)
436
439
  scope = _scope(account, url)
437
440
  mine = paths.user_dir(data, user)
@@ -439,11 +442,18 @@ def login(
439
442
  report = SessionReport(user, adapter, scope, "missing", session)
440
443
  if create and not email:
441
444
  raise ValueError("creating an account needs --email")
442
- if email:
445
+ try:
446
+ kept = create and passwords.stored(mine, adapter, scope) is not None
447
+ except passwords.PasswordError as e:
448
+ report.status, report.reason = Status.BLOCKED, f"password_source: {e}"
449
+ return report
450
+ if email and not kept:
443
451
  accounts.remember(mine, adapter, scope, email)
444
- if create:
452
+ if create and not kept:
445
453
  assert email
446
- return _create(report, account, url, mine, email, data)
454
+ return _create(report, account, url, mine, (email, name), data)
455
+ if kept:
456
+ report.password = "kept"
447
457
  if password and not (isinstance(account, SignsIn) and accounts.email(mine, adapter, scope)):
448
458
  raise ValueError(f"signing in to {adapter} with a password needs --email")
449
459
  try:
@@ -473,7 +483,7 @@ def _create(
473
483
  account: NeedsAccount,
474
484
  url: str | None,
475
485
  mine: Path,
476
- email: str,
486
+ who: tuple[str, str | None],
477
487
  data: Path,
478
488
  ) -> SessionReport:
479
489
  """Creates the account: the generated password is stored before the click, and deleted
@@ -482,13 +492,16 @@ def _create(
482
492
  report.status = Status.UNSUPPORTED
483
493
  report.reason = f"no_sign_up: atsfill cannot create a {account.name} account"
484
494
  return report
495
+ email, name = who
496
+ if account.asks_name and not name:
497
+ raise ValueError(f"creating a {account.name} account needs --name")
485
498
  password = passwords.generate()
486
499
  try:
487
500
  passwords.store(mine, account.name, report.scope, password)
488
501
  except passwords.PasswordError as e:
489
502
  report.status, report.reason = Status.BLOCKED, f"password_source: {e}"
490
503
  return report
491
- result = _headless(data, account, report.scope, (email, password), report.path, create=True)
504
+ result = _headless(data, account, report.scope, (email, password), report.path, True, name)
492
505
  command = login_command(account.name, report.user, url)
493
506
  report.status, report.reason = result.status, _email_link(result.reason, email, command)
494
507
  report.password = "stored"
@@ -507,6 +520,7 @@ def _headless(
507
520
  credentials: Credentials,
508
521
  session: Path,
509
522
  create: bool = False,
523
+ name: str | None = None,
510
524
  ) -> Result:
511
525
  """Signs in, or creates the account, through the adapter: `ready` once the session is
512
526
  stored."""
@@ -516,7 +530,7 @@ def _headless(
516
530
  def run(_: list[str]) -> Result:
517
531
  result = Result(Status.READY, url, account.name)
518
532
  with open_page(data, None) as page, _stops(result):
519
- accounts.sign_in(page, account, scope, *credentials, session, create)
533
+ accounts.sign_in(page, account, scope, *credentials, session, create, name)
520
534
  return result
521
535
 
522
536
  return _in_budget(url, run)
@@ -34,7 +34,7 @@ class Field:
34
34
  options: list[str] | None = None
35
35
  max_length: int | None = None
36
36
  accept: list[str] | None = None
37
- prefilled: bool = False # holds a value before any answer, from the account's profile
37
+ prefilled: bool = False # holds a value before any answer, from the account profile
38
38
  unfillable: str | None = None # why atsfill cannot fill it, a reason (ADR 0014)
39
39
 
40
40
 
@@ -75,7 +75,7 @@ class SessionReport:
75
75
  path: Path
76
76
  status: str | None = None # a login's outcome: signed_in | refused | blocked | unsupported
77
77
  reason: str | None = None # why a login stored nothing, `code` or `code: detail`
78
- password: str | None = None # after `--create`, the generated one: stored | deleted
78
+ password: str | None = None # after `--create`: stored | deleted, or kept
79
79
 
80
80
 
81
81
  @dataclass
@@ -13,7 +13,7 @@
13
13
  # send_verified
14
14
  # unsupported
15
15
  # date, version, commit, by, reason: a round that ended without the adapter passing
16
- # the gate (ADR 0013), the known gap that stopped it
16
+ # the adapter gate (ADR 0013), the known gap that stopped it
17
17
  # out_of_scope
18
18
  # reason, date: a system atsfill will never fill, it needs the user live (ADR 0014)
19
19
  # send_broken
@@ -53,7 +53,7 @@ account = "to_send"
53
53
  hosting = ["hosted"]
54
54
  adapter = "wttj"
55
55
  send_verified = { date = 2026-10-20, version = "0.0.3", by = "shbernal", commit = "9bc6d84" }
56
- dry_run_verified = { date = 2026-10-19, version = "0.0.2", by = "adapter_gate", commit = "ad9f478" }
56
+ dry_run_verified = { date = 2026-10-24, version = "0.0.3", by = "adapter_gate", commit = "90c58c3" }
57
57
 
58
58
  [workday]
59
59
  kind = "ats"
@@ -62,6 +62,7 @@ account = "to_read"
62
62
  hosting = ["hosted"]
63
63
  adapter = "workday"
64
64
  mapped = { date = 2026-10-20, version = "0.0.2", by = "agent", commit = "7934b25" }
65
+ dry_run_verified = { date = 2026-10-24, version = "0.0.3", by = "adapter_gate", commit = "fdb237a" }
65
66
 
66
67
  [trudvsem]
67
68
  kind = "board"
@@ -315,7 +316,7 @@ dry_run_verified = { date = 2026-10-22, version = "0.0.3", by = "adapter_gate",
315
316
  [isolvedhire]
316
317
  kind = "ats"
317
318
  share = 0.0047
318
- account = "to_read" # Apply now first verifies the email or phone with a one-time code, then creates an account on the employer's careers site; the application wizard (/apply/<job>-0.php) redirects to that site's login until then
319
+ account = "to_read" # Apply now first verifies the email or phone with an OTP, then creates an account on the employer's careers site; the application wizard (/apply/<job>-0.php) redirects to that site's login until then
319
320
  hosting = ["hosted"]
320
321
  mapped = { date = 2026-10-20, version = "0.0.2", by = "agent", commit = "7934b25" }
321
322
 
File without changes