Job search runbook
Not user-facing; job-search takes user questions. Mechanics the job-search skills read before a run: the workspace-discovery and validate-workspace scripts, the file table, how a run opens and closes.From its SKILL.md
npx -y skills add agent-data/job-search --skill job-search-runbookAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
SKILL.md
9.2 KB, as published. Nobody here has run it
job-search-runbook — workspace, run contract, scratch
The mechanics every flow needs: where the user's workspace is, what each file in it holds, how one run opens and closes, and what stays off disk.
Find the workspace
Every path below that names a file in the user's workspace is relative to the workspace this step
prints. The few that name a file inside the plugin instead say so where they are written. Run the
plugin's skills/job-search-runbook/scripts/workspace-discovery.sh.
It prints three key=value lines on stdout:
workspace=/Users/dana/job-search
source=legacy
first_run=false
It writes one more line to stderr, naming the registry path and saying what it found there, so a terminal or a tool result that merges the two streams shows four lines. Read it together with the parse-check rule at the end of this section.
first_run=true means that path has no config.yaml yet and setup creates it. source=legacy means
the workspace sits at the older visible path: keep using it, and record it in the registry as
active_workspace so later runs find it. An existing config.yaml, preferences.md, or jobs.jsonl
stays as it is — adopting a workspace adds only the missing runs/ and reports/ directories.
The registry file — the one to read here and to write for adoption or scheduling — is
$JOBSEARCH_OS_REGISTRY when set, otherwise
${XDG_CONFIG_HOME:-${JOBSEARCH_OS_HOME:-$HOME}/.config}/job-search/config.json.
Where no shell runs, read that file and apply the same order yourself. In the rules below, $H is
$JOBSEARCH_OS_HOME when set and $HOME otherwise (tests and evals set it to move the home base).
First match decides:
- The registry parses as JSON and holds a non-empty
active_workspace— use it and stop, even with noconfig.yamlthere (that is a first run); falling through could move the user elsewhere. $H/.job-search/config.yamlexists — workspace$H/.job-search.$H/job-search/config.yamlexists — workspace$H/job-search.- Neither exists — first run, workspace
$H/.job-search, which setup creates.
A registry file that exists but does not parse as JSON stops the run: report that the file at that path cannot be read, rather than picking a workspace it might not name. A registry the script could not look at stops the run the same way, and the workspace on stdout came from the rules below rather than from the registry.
What each file holds
| File | Written by | Read by |
|---|---|---|
config.yaml — queries, sources, schedule | setup, then the user by hand | every run, the home view |
preferences.md — the brief, in prose | the preference interview | every run, every fit judgment |
jobs.jsonl — append-only event log, one JSON object per line. A run writes a call event for each agent-data request and surfaced, queued, detail and evaluated events about postings. A posting has several lines, all carrying the same source and source_id, and its current state is its last evaluated line. | every run | the home view, duplicate checks |
runs/<run_id>.json — one record per run; <run_id> is the run's UTC start time with dashes for the colons, like 2026-07-30T15-04-02Z, so it works as a filename | skills/job-search-runbook/scripts/close-run.sh | the home view, the agent skill |
runs/.started-<run_id> — empty marker: this run is open | skills/job-search-runbook/scripts/open-run.sh, deleted by skills/job-search-runbook/scripts/clear-run.sh | the next run |
reports/<date>-digest.md — the digest the user reads | a run at close | the user, the home view |
.gitignore — copied from the plugin's skills/job-search/templates/workspace.gitignore; denies everything but itself | setup | git |
~/.config/job-search/config.json — the registry, which sits outside the workspace: active_workspace, plus scheduling holding the booleans installed and verified — both true only after a canary proved the job — and the strings mechanism (cron, launchd, or the harness's own) and scheduler_id | setup, schedule changes | discovery, the home view |
One run, start to close
- Look for a leftover marker —
ls <workspace>/runs/.started-* 2>/dev/null. A marker there belongs to a run that stopped before it could close. Say that the last run did not finish, then close and clear it before going on with this run: firstskills/job-search-runbook/scripts/close-run.sh <workspace> <run_id> --trigger manual --close-state interrupted, thenskills/job-search-runbook/scripts/clear-run.sh <workspace> <run_id>, taking<run_id>off the marker's name. Passmanualbecause nothing on disk records what started a run that never closed —triggeris written only into the run record, which is the file that run never wrote — andscheduledis what the scheduling canary reads as proof the scheduler ran. Deleting the marker instead leaves nothing recording that the run happened, which is whyskills/job-search-runbook/scripts/clear-run.shrefuses a run with no record. - Open the run — run the plugin's
skills/job-search-runbook/scripts/open-run.sh <workspace>. It printsrun_id,started_atandbrief_revisionon stdout, in that order; carry all three through the run. Exit 1 means the run is open — the marker is on disk and those three lines are printed — and something is wrong that this run cannot fix: read stdout after the three lines for the findings about the workspace files, and stderr for the case where the brief's revision could not be taken. Report whichever you got in plain language and close the runblocked. Exit 2 means the run did not open and nothing was written; stderr says why. A run already open means an earlier run's marker is still inruns/: go back to step 1, which says what to tell the user, then open this one. - Do the run's work, recording events as you go.
- Close, in this order. First
skills/job-search-runbook/scripts/close-run.sh <workspace> <run_id> --trigger … --close-state …, with--brief-revisionfrom step 2,--sourcesand--queriesfrom the run, and--scheduler-idwhen a scheduler started it. It works outrun_healthand prints it. Then the digest, which the run skill defines. Thenskills/job-search-runbook/scripts/clear-run.sh <workspace> <run_id>, which deletes the marker and the scratch directory. Thenskills/job-search-runbook/scripts/validate-workspace.sh <workspace> --post-close <run_id>, fixing whatever it prints on stdout.
The digest is written between the record and the clearing because it carries run_health off the
record and may still need the responses in the scratch directory.
trigger, scheduler_id and close_state are the only values in the record you decide. Every
count and both timestamps come from jobs.jsonl and the clock.
A run that has to stop early closes the same way: close_state is blocked when you can name what
stopped it, and interrupted when it ends unfinished and its work cannot be reconstructed.
Running it unattended
A scheduler starts a run by invoking this pack's job-search-run skill
through the harness's non-interactive command instead of a live session. Compose the command from what your harness documents: its
non-interactive runner (the subcommand or flag that takes a prompt without a terminal), the
permissions that let the run write the workspace and call the agent-data CLI with nobody watching,
and the name your harness lists the job-search-run skill under — harnesses differ on whether the pack's
name prefixes it. Write into the job's prompt that a scheduler started it and that job's id, so the
record closes with trigger: scheduled and that scheduler_id. A harness with no non-interactive
command has nothing for a schedule to start: say so, and an in-session loop is the fallback.
A cron or launchd process has no login session, so the job authenticates the way your harness's
documentation says a non-interactive run authenticates — an environment variable the job's
definition carries, a stored token, or the same store the session uses. Put the credential where
that documentation says it lives, and nowhere in the workspace. Whether the credentials actually reach a scheduled process is settled by
the job-search skill's canary. Judge the scheduled run by its record rather
than the process exit code: harnesses differ on whether that code reflects the run.
Scratch
A run's own working files go in runs/.scratch/<run_id>/: each search row as it arrived, id and
source_url paired for the detail read.
What stays off disk
The workspace holds a private job search. Write none of these to a file: API keys and auth headers,
pagination cursors, preference text anywhere but preferences.md. A posting's own text is not on
that list: it is stored on the posting's detail event, so a changed brief can be re-applied by
judging that stored text again instead of paying to read the same postings a second time.