# gimme-job Learn Mode — System Instructions You are working inside the **gimme-job** codebase. ## Your Goal Create or update a job site adapter so that `gimme-job run --site {site_id}` works without any AI assistance at runtime. ## Core Rules 1. **Runtime must not require AI.** The adapter must work with pure Python + Playwright. 2. **Use Chrome persistent profile `JobAgent`** — never `browser.new_context()` directly. 3. **No storage_state** — Chrome profile reuse is the only session persistence mechanism. 4. **Prefer robust selectors**: `aria-label`, `data-*` attributes, semantic HTML. Avoid brittle `nth-child`. 5. **Always define fallback selectors** (list multiple per field in the manifest). 6. **Handle zero-result states explicitly** — `ZERO_RESULTS_EXPECTED` is a normal exit. 7. **No detail page visits by default** — extract from card list only. 8. **If a date filter exists in the UI, use it.** ## Files to Generate For site_id `{site_id}`: 1. `sites/{site_id}.yaml` — Site manifest YAML 2. `gimme_job/adapters/{site_id}.py` — Adapter Python file 3. `tests/adapters/test_{site_id}.py` — Smoke test 4. `workspace/manifests/{site_id}.learning-report.md` — Learning report ## Adapter Interface The adapter must implement `BaseJobSiteAdapter` from `gimme_job/adapters/base.py`: ```python class BaseJobSiteAdapter(Protocol): site_id: str def prepare(self, page: Page, config: SiteManifest) -> None: ... def apply_search(self, page: Page, query: SearchQuery) -> None: ... def apply_filters(self, page: Page, query: SearchQuery) -> None: ... def collect_cards(self, page: Page, config: SiteManifest) -> list[RawJobCard]: ... def paginate(self, page: Page, page_index: int, config: SiteManifest) -> bool: ... def normalize(self, raw: RawJobCard) -> JobPostingCandidate: ... ``` Use `@register("{site_id}")` from `gimme_job/adapters/registry.py` to register the adapter. ## Manifest Schema Key YAML fields (see `gimme_job/models/manifest.py` for full Pydantic schema): ```yaml site_id: {site_id} enabled: true repair_needed: false identity: label: "Site Name" base_url: "https://..." start_url_template: "https://...?q={keywords_urlencoded}" browser: profile_mode: persistent_chrome_profile profile_name: JobAgent headed_on_learn: true headless_on_run: true search: keyword_mode: url_param # url_param | input_field | none date_mode: none # none | url_param | click_filter date_filter_text: null result_list_wait_selector: null pagination: mode: none # none | next_button | url_increment next_button_selectors: [] max_pages: 3 extract: container_selectors: - "CSS selector for each job card container" fields: title: text: ["h2 a", "h3"] company: text: [".company-name"] location: text: [".location"] posted_text: text: ["time", ".date"] url: attr: selector: "a[href*='/jobs/']" name: href post_filters: include_posted_text: - "today" - "1 day ago" ``` ## Smoke Test Requirements The smoke test in `tests/adapters/test_{site_id}.py` must: 1. Open the job search page 2. Either detect result cards OR confirm zero-result state 3. Extract at least 2 fields from a card (title + one other) OR confirm zero results 4. Pass with `pytest tests/adapters/test_{site_id}.py` ## Acceptance Criteria Learning is successful when ALL of these are true: - `sites/{site_id}.yaml` exists and is valid - `gimme_job/adapters/{site_id}.py` exists with no syntax errors - `tests/adapters/test_{site_id}.py` exists - `gimme-job test {site_id}` passes (smoke test) - At least 1 field extraction logic is verified