Reconciles tracker.yaml from the filesystem, renders a statusboard, and — on
Claude Code — drives a two-level menu loop (pick an application, then pick an
action). On Codex it renders a read-only board with suggested next actions.
Flags:
--board-only— reconcile and render the board, then stop (no menu loop) on any platform.--fast— skip the reconcile entirely: render the board from the tracker exactly as stored, then stop (no scan, no tracker write, no menu loop). Use for a quick read-only glance; artifact flags andnext_actionreflect the last full reconcile, not the current filesystem. Takes precedence over--board-only(both stop after the board). If--fastand--board-onlyare passed together,--fastwins.
Configuration
Read .jobops/config.json. If missing, stop with:
JOBOPS NOT CONFIGURED Run /jobops:setup to initialize your workspace.
Use config.directories.<key> for all file paths in this skill.
Tracker path resolution (self-heal): read config.directories.application_tracker.
If that key is absent, set it to {config.directories.applications_root}/tracker.yaml,
load the full existing config object, add only directories.application_tracker (leaving
every other key — directories.*, preferences, candidate, templates, migration —
untouched), and write the whole object back atomically to .jobops/config.json.tmp in the
same directory, then mv over .jobops/config.json, and use that path. This lets
workspaces created before the dashboard existed work with no migration step.
Under --fast, resolve the path read-only: if the key is absent, use the default
{config.directories.applications_root}/tracker.yaml without writing config back (a fast
glance never mutates state). If no tracker file exists at the resolved path, print
No tracker yet — run /jobops:dashboard (without --fast) to build it. and exit.
Reconcile
Run this every invocation, before rendering — unless --fast was passed, in which case
skip this entire section and render the board directly from the stored tracker (no scan, no
recompute, no write).
-
Load the existing tracker YAML at the resolved path, or start with
{version: 1, applications: []}if the file does not exist. -
Scan
config.directories.applications_rootfor immediate sub-directories whose name is not dot-prefixed. Each such folder is an application keyed by its name (slug). For each slug, set theartifactsmap using the resolution order below — first match wins. The canonical path is checked first; theoutput_typeYAML front-matter key is the durable fallback (trust it over the filename); a filename glob is the last-resort safety net for files written before producers were standardized. Scan the whole app folder (recursively), not just the expected sub-folder, so flat-layout legacy apps still register. When testingoutput_type, read the first front-matter block (the file's first ~30 lines).Flag True when (first match wins) assessmentassessment/assessment.mdexists, OR any.mdin the app hasoutput_type: assessmentresume_draftresume/step1_draft.mdorresume/step2_provenance.mdexists, OR any.mdhasoutput_type ∈ {resume_step1, resume_provenance}, OR globresume/*step1*.md,resume/*draft*.md,resume/*provenance*.mdmatchesresume_finalresume/step3_final.mdexists, OR any.mdhasoutput_type: resume_final, OR globresume/*step3*final*.md/resume/*final*.mdmatches (excluding*provenance*)cover_lettercover-letter/cover_letter.mdexists, OR any.mdhasoutput_type: cover_letter, OR globcover-letter/*cover*letter*.mdmatchesosintthe detected company folder (below) exists under company_intelligencebriefingglob interview/briefing*.mdmatches, OR any.mdhasoutput_type: briefinginterview_prepglob interview/interview_prep*.mdmatches, OR any.mdhasoutput_type: interview_prepIf
applications_rootdoes not exist or contains no application folders, treat the scan as yielding zero apps (do not error) and continue — existing tracker entries are still archived per step 5 and an empty board is rendered. -
Detect company / role for each slug:
- Drop a trailing 8-digit date token (
_YYYYMMDD) from the slug. The remainder, with_→ space, is the humanized title. A folder whose name has no trailing 8-digit date is still a valid app; its full humanized name becomes the title. - List sub-folders of
company_intelligence. Slugify each folder name (spaces,&,-,,→_; collapse repeats; trim_) and lowercase. If a slugified folder nameCsatisfieslower(slug) == Corlower(slug)starts withC + "_", the company is that folder's name andosintis true. Otherwisecompanyis"". role: if a company was detected, the humanized title with the company's leading tokens removed; otherwise the full humanized title.
- Drop a trailing 8-digit date token (
-
Merge each scanned slug into the tracker:
- Existing slug: keep every human-editable field verbatim —
company,role,stage,applied_date,next_deadline,contact,outcome,notes. (Role humanized from a slug is lossy, e.g.SeniorPM, so the user's refinedcompany/roleare preserved once set.) Overwrite onlyartifactsandnext_actionfrom the scan. - New slug: append
{slug, company, role, stage: lead, applied_date: null, next_deadline: null, contact: null, outcome: null, notes: null, artifacts, next_action}using the derivedcompany/role.leadis the default stage in the absence of any other information — the user advances it via Update status as the application progresses.
- Existing slug: keep every human-editable field verbatim —
-
Archive tracker entries whose slug no longer has a folder: set
stage: archived, allartifactsfalse,next_action: none. Keep all human-status fields. -
Compute
next_action(first false wins; skip for archived →none):assessment == false → assessjob resume_final == false → buildresume cover_letter == false → coverletter osint == false → osint briefing == false → briefing interview_prep== false → interviewprep else → record-outcome -
Sort
applicationsbyslugascending. Set top-levelgeneratedto the current UTC time in ISO-8601 (YYYY-MM-DDTHH:MM:SSZ). -
Write the YAML back atomically: write
tracker.yaml.tmpin the same directory astracker.yaml, thenmvit overtracker.yamlso the rename is atomic. Emit the two zones in this field order per entry:slug, company, role, stage, applied_date, next_deadline, contact, outcome, notes, artifacts, next_action; theartifactsmap in this order:assessment, resume_draft, resume_final, cover_letter, osint, briefing, interview_prep.
Board render
After reconcile (or, under --fast, directly from the stored tracker — the header's
reconciled {generated} then reflects the last full reconcile, signalling staleness),
print a summary line then a table sorted by stage rank
(interviewing, offer, applied, preparing, lead, on_hold, withdrawn, rejected, archived),
then by next_deadline ascending (nulls last). Count of non-archived apps in the header.
JobOps Application Tracker · {N} active · reconciled {generated}
Stage Company / Role Pipeline Next Deadline
───────────── ───────────────────────── ───────────── ─────────────── ──────────
{stage} {company / role} {dots} {next_action} {deadline}{warn}
Pipeline dots: assess▸resume▸cover▸osint▸briefing▸prep
- Company / Role cell:
"{company} / {role}"when company is non-empty, else the humanized title alone. - Dots — six glyphs in order
assess · resume · cover · osint · briefing · prep:●when that flag is true; for resume use◐whenresume_draft && !resume_final;○otherwise. Archived rows show——. - Deadline:
next_deadlineasMon D, or—if null. Append⚠when the deadline is within 3 days ofgenerated.
Interaction
If --board-only or --fast was passed, stop after the board (no menu loop).
Claude Code (AskUserQuestion available): loop until the user backs out.
- Render the board.
- Navigate — ask one
AskUserQuestionwhose options are the up-to-3 apps with a pendingnext_action(i.e. notrecord-outcome/none), sorted bynext_deadlineascending. Label"{company-or-title} — {role}"; description"{stage} · next: {next_action}". The tool's auto "Other" lets the user type a slug or table row number for any other app. Resolve the selection to one application. Apps already atrecord-outcomeare reachable only via the auto "Other" option (the navigate picker excludes them); selecting one jumps straight to the outcome fields. - Act — ask a second
AskUserQuestionwith ≤4 options:Run next: {next_action}— invoke that skill (/jobops:{next_action}, or forrecord-outcomego straight to the outcome update below), passing the app folder{applications_root}/{slug}as context. On return, re-run Reconcile and loop.Update status— ask follow-upAskUserQuestion(s) for the field to change (stage,next_deadline,contact,outcome,notes), write the new value into that app's human-status zone, re-write the YAML atomically, and loop. Forstage, the choices are the pipeline stages in lifecycle order —lead → preparing → applied → interviewing → offer— plus the side stateson_hold,withdrawn, andrejected(archivedis set automatically when the app folder disappears, not chosen here).Open folder— print{applications_root}/{slug}and list its contents.Back to board— redraw the board; if chosen again at the top level, exit.
Codex (no AskUserQuestion): render the board, then a numbered list of the apps with a
pending next_action and the suggested skill for each, and stop with:
Invoke the suggested skill manually, e.g.
/jobops:buildresume.
Notes
- This skill is the only reader/writer of
tracker.yaml. Other skills never touch it; their outputs are picked up on the next reconcile. - Never delete tracker entries — archive them, preserving human-status fields.