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.
1---2name: dashboard3description: Render an application statusboard from a reconciled YAML tracker and (on Claude Code) drive an interactive navigate-and-act loop over the application pipeline4---56# JobOps Application Dashboard78Reconciles `tracker.yaml` from the filesystem, renders a statusboard, and — on9Claude Code — drives a two-level menu loop (pick an application, then pick an10action). On Codex it renders a read-only board with suggested next actions.1112**Flags:**13- `--board-only` — reconcile and render the board, then stop (no menu loop) on any platform.14- `--fast` — skip the reconcile entirely: render the board from the tracker **exactly as15 stored**, then stop (no scan, no tracker write, no menu loop). Use for a quick read-only16 glance; artifact flags and `next_action` reflect the last full reconcile, not the current17 filesystem. Takes precedence over `--board-only` (both stop after the board). If `--fast`18 and `--board-only` are passed together, `--fast` wins.1920---2122## Configuration2324Read `.jobops/config.json`. If missing, stop with:2526> JOBOPS NOT CONFIGURED27> Run /jobops:setup to initialize your workspace.2829Use `config.directories.<key>` for all file paths in this skill.3031**Tracker path resolution (self-heal):** read `config.directories.application_tracker`.32If that key is absent, set it to `{config.directories.applications_root}/tracker.yaml`,33load the full existing config object, add only `directories.application_tracker` (leaving34every other key — `directories.*`, `preferences`, `candidate`, `templates`, `migration` —35untouched), and write the whole object back atomically to `.jobops/config.json.tmp` in the36same directory, then `mv` over `.jobops/config.json`, and use that path. This lets37workspaces created before the dashboard existed work with no migration step.3839Under `--fast`, resolve the path **read-only**: if the key is absent, use the default40`{config.directories.applications_root}/tracker.yaml` without writing config back (a fast41glance never mutates state). If no tracker file exists at the resolved path, print42`No tracker yet — run /jobops:dashboard (without --fast) to build it.` and exit.4344---4546## Reconcile4748Run this every invocation, before rendering — **unless `--fast` was passed**, in which case49skip this entire section and render the board directly from the stored tracker (no scan, no50recompute, no write).51521. **Load** the existing tracker YAML at the resolved path, or start with53 `{version: 1, applications: []}` if the file does not exist.542. **Scan** `config.directories.applications_root` for immediate sub-directories whose55 name is not dot-prefixed. Each such folder is an application keyed by its name (`slug`).56 For each slug, set the `artifacts` map using the resolution order below — **first match57 wins**. The canonical path is checked first; the `output_type` YAML front-matter key is the58 **durable** fallback (trust it over the filename); a filename glob is the last-resort safety59 net for files written before producers were standardized. Scan the **whole app folder**60 (recursively), not just the expected sub-folder, so flat-layout legacy apps still register.61 When testing `output_type`, read the first front-matter block (the file's first ~30 lines).62 | Flag | True when (first match wins) |63 |---|---|64 | `assessment` | `assessment/assessment.md` exists, **OR** any `.md` in the app has `output_type: assessment` |65 | `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 |66 | `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*`) |67 | `cover_letter` | `cover-letter/cover_letter.md` exists, **OR** any `.md` has `output_type: cover_letter`, **OR** glob `cover-letter/*cover*letter*.md` matches |68 | `osint` | the detected company folder (below) exists under `company_intelligence` |69 | `briefing` | glob `interview/briefing*.md` matches, **OR** any `.md` has `output_type: briefing` |70 | `interview_prep` | glob `interview/interview_prep*.md` matches, **OR** any `.md` has `output_type: interview_prep` |7172 If `applications_root` does not exist or contains no application folders, treat the scan73 as yielding zero apps (do not error) and continue — existing tracker entries are still74 archived per step 5 and an empty board is rendered.753. **Detect company / role** for each slug:76 - Drop a trailing 8-digit date token (`_YYYYMMDD`) from the slug. The remainder,77 with `_` → space, is the **humanized title**. A folder whose name has no trailing78 8-digit date is still a valid app; its full humanized name becomes the title.79 - List sub-folders of `company_intelligence`. Slugify each folder name (spaces, `&`,80 `-`, `,` → `_`; collapse repeats; trim `_`) and lowercase. If a slugified folder name81 `C` satisfies `lower(slug) == C` or `lower(slug)` starts with `C + "_"`, the company is82 that folder's name and `osint` is true. Otherwise `company` is `""`.83 - `role`: if a company was detected, the humanized title with the company's leading84 tokens removed; otherwise the full humanized title.854. **Merge** each scanned slug into the tracker:86 - **Existing slug:** keep every human-editable field verbatim — `company`, `role`,87 `stage`, `applied_date`, `next_deadline`, `contact`, `outcome`, `notes`. (Role88 humanized from a slug is lossy, e.g. `SeniorPM`, so the user's refined `company`/`role`89 are preserved once set.) Overwrite only `artifacts` and `next_action` from the scan.90 - **New slug:** append `{slug, company, role, stage: lead, applied_date: null,91 next_deadline: null, contact: null, outcome: null, notes: null, artifacts, next_action}`92 using the derived `company`/`role`. `lead` is the default stage in the absence of any93 other information — the user advances it via **Update status** as the application94 progresses.955. **Archive** tracker entries whose slug no longer has a folder: set `stage: archived`,96 all `artifacts` false, `next_action: none`. Keep all human-status fields.976. **Compute `next_action`** (first false wins; skip for archived → `none`):98 ```99 assessment == false → assessjob100 resume_final == false → buildresume101 cover_letter == false → coverletter102 osint == false → osint103 briefing == false → briefing104 interview_prep== false → interviewprep105 else → record-outcome106 ```1077. **Sort** `applications` by `slug` ascending. Set top-level `generated` to the current108 UTC time in ISO-8601 (`YYYY-MM-DDTHH:MM:SSZ`).1098. **Write** the YAML back atomically: write `tracker.yaml.tmp` in the same directory as110 `tracker.yaml`, then `mv` it over `tracker.yaml` so the rename is atomic. Emit the two zones in111 this field order per entry: `slug, company, role, stage, applied_date, next_deadline,112 contact, outcome, notes, artifacts, next_action`; the `artifacts` map in this order:113 `assessment, resume_draft, resume_final, cover_letter, osint, briefing, interview_prep`.114115---116117## Board render118119After reconcile (or, under `--fast`, directly from the stored tracker — the header's120`reconciled {generated}` then reflects the *last* full reconcile, signalling staleness),121print a summary line then a table sorted by stage rank122(`interviewing, offer, applied, preparing, lead, on_hold, withdrawn, rejected, archived`),123then by `next_deadline` ascending (nulls last). Count of non-archived apps in the header.124125```126JobOps Application Tracker · {N} active · reconciled {generated}127128Stage Company / Role Pipeline Next Deadline129───────────── ───────────────────────── ───────────── ─────────────── ──────────130{stage} {company / role} {dots} {next_action} {deadline}{warn}131132Pipeline dots: assess▸resume▸cover▸osint▸briefing▸prep133```134135- **Company / Role cell:** `"{company} / {role}"` when company is non-empty, else the136 humanized title alone.137- **Dots** — six glyphs in order `assess · resume · cover · osint · briefing · prep`:138 `●` when that flag is true; for resume use `◐` when `resume_draft && !resume_final`;139 `○` otherwise. Archived rows show `——`.140- **Deadline:** `next_deadline` as `Mon D`, or `—` if null. Append ` ⚠` when the deadline141 is within 3 days of `generated`.142143---144145## Interaction146147If `--board-only` or `--fast` was passed, stop after the board (no menu loop).148149**Claude Code (AskUserQuestion available):** loop until the user backs out.1501. Render the board.1512. **Navigate** — ask one `AskUserQuestion` whose options are the up-to-3 apps with a152 pending `next_action` (i.e. not `record-outcome`/`none`), sorted by `next_deadline`153 ascending. Label `"{company-or-title} — {role}"`; description `"{stage} · next: {next_action}"`.154 The tool's auto "Other" lets the user type a slug or table row number for any other app.155 Resolve the selection to one application. Apps already at `record-outcome` are reachable156 only via the auto "Other" option (the navigate picker excludes them); selecting one jumps157 straight to the outcome fields.1583. **Act** — ask a second `AskUserQuestion` with ≤4 options:159 - `Run next: {next_action}` — invoke that skill (`/jobops:{next_action}`, or for160 `record-outcome` go straight to the outcome update below), passing the app folder161 `{applications_root}/{slug}` as context. On return, re-run **Reconcile** and loop.162 - `Update status` — ask follow-up `AskUserQuestion`(s) for the field to change163 (`stage`, `next_deadline`, `contact`, `outcome`, `notes`), write the new value into164 that app's human-status zone, re-write the YAML atomically, and loop. For `stage`, the165 choices are the pipeline stages in lifecycle order — `lead → preparing → applied →166 interviewing → offer` — plus the side states `on_hold`, `withdrawn`, and `rejected`167 (`archived` is set automatically when the app folder disappears, not chosen here).168 - `Open folder` — print `{applications_root}/{slug}` and list its contents.169 - `Back to board` — redraw the board; if chosen again at the top level, exit.170171**Codex (no AskUserQuestion):** render the board, then a numbered list of the apps with a172pending `next_action` and the suggested skill for each, and stop with:173174> Invoke the suggested skill manually, e.g. `/jobops:buildresume`.175176---177178## Notes179180- This skill is the only reader/writer of `tracker.yaml`. Other skills never touch it;181 their outputs are picked up on the next reconcile.182- Never delete tracker entries — archive them, preserving human-status fields.