| name | dashboard |
| description | Render an application statusboard from a reconciled YAML tracker and (on Claude Code) drive an interactive navigate-and-act loop over the application pipeline |
| disable-model-invocation | true |
| argument-hint | [--board-only] [--fast] |
JobOps Application Dashboard
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 and next_action reflect the last full reconcile, not the current
filesystem. Takes precedence over --board-only (both stop after the board). If --fast
and --board-only are passed together, --fast wins.
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_root for immediate sub-directories whose
name is not dot-prefixed. Each such folder is an application keyed by its name (slug).
For each slug, set the artifacts map using the resolution order below — first match
wins. The canonical path is checked first; the output_type YAML 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 testing output_type, read the first front-matter block (the file's first ~30 lines).
| Flag | True when (first match wins) |
|---|
assessment | assessment/assessment.md exists, OR any .md in the app has output_type: assessment |
resume_draft | resume/step1_draft.md or resume/step2_provenance.md exists, OR any .md has output_type ∈ {resume_step1, resume_provenance}, OR glob resume/*step1*.md, resume/*draft*.md, resume/*provenance*.md matches |
resume_final | resume/step3_final.md exists, OR any .md has output_type: resume_final, OR glob resume/*step3*final*.md / resume/*final*.md matches (excluding *provenance*) |
cover_letter | cover-letter/cover_letter.md exists, OR any .md has output_type: cover_letter, OR glob cover-letter/*cover*letter*.md matches |
osint | the detected company folder (below) exists under company_intelligence |
briefing | glob interview/briefing*.md matches, OR any .md has output_type: briefing |
interview_prep | glob interview/interview_prep*.md matches, OR any .md has output_type: interview_prep |
If applications_root does 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 name
C satisfies lower(slug) == C or lower(slug) starts with C + "_", the company is
that folder's name and osint is true. Otherwise company is "".
role: if a company was detected, the humanized title with the company's leading
tokens removed; otherwise the full humanized title.
-
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 refined company/role
are preserved once set.) Overwrite only artifacts and next_action from 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 derived company/role. lead is the default stage in the absence of any
other information — the user advances it via Update status as the application
progresses.
-
Archive tracker entries whose slug no longer has a folder: set stage: archived,
all artifacts false, 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 applications by slug ascending. Set top-level generated to the current
UTC time in ISO-8601 (YYYY-MM-DDTHH:MM:SSZ).
-
Write the YAML back atomically: write tracker.yaml.tmp in the same directory as
tracker.yaml, then mv it over tracker.yaml so 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; the artifacts map 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 ◐ when resume_draft && !resume_final;
○ otherwise. Archived rows show ——.
- Deadline:
next_deadline as Mon D, or — if null. Append ⚠ when the deadline
is within 3 days of generated.
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
AskUserQuestion whose options are the up-to-3 apps with a
pending next_action (i.e. not record-outcome/none), sorted by next_deadline
ascending. 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 at record-outcome are reachable
only via the auto "Other" option (the navigate picker excludes them); selecting one jumps
straight to the outcome fields.
- Act — ask a second
AskUserQuestion with ≤4 options:
Run next: {next_action} — invoke that skill (/jobops:{next_action}, or for
record-outcome go 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-up AskUserQuestion(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. For stage, the
choices are the pipeline stages in lifecycle order — lead → preparing → applied → interviewing → offer — plus the side states on_hold, withdrawn, and rejected
(archived is 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.