Schedule Dispatcher
Purpose
One task, one report, one diary entry per job. The dispatcher replaces the previous model of 5 standalone scheduled tasks. It is invoked once per day, reads configuration, and runs whichever maintenance jobs are due.
The dispatcher never decides on its own to change frequencies or skip jobs that
are due — it surfaces suggestions in the morning report and waits for the user
to approve a change (via the schedule-tune skill).
Preconditions
Before doing anything, verify the working directory is a BCOS-enabled repo:
- Confirm
.claude/quality/schedule-config.jsonexists - Confirm
docs/exists - Confirm
.claude/hook_state/exists (create if missing — used for diary)
If any are missing, stop and report: "Dispatcher invoked outside a BCOS repo, or BCOS is not fully installed. Run python .claude/scripts/update.py first."
Step 1: Read Configuration
Read .claude/quality/schedule-config.json. Validate required fields:
version(string)jobs(object) — each entry must haveenabled(bool) andschedule(string)auto_fix.enabled(bool)auto_fix.whitelist(array of strings)digest(object) —write_file(bool) andpath(string)
If the config is malformed, STOP. Emit a typed framework-config-malformed finding (schema 1.1.0+, category: "bcos-framework") with finding_attrs = {file: ".claude/quality/schedule-config.json", parse_error: "{message}" | null, missing_fields: [...] | null}. Write the minimum-shape sidecar (with this finding + overall_verdict: "red" + empty jobs[]) AND a framework-config-malformed row to .claude/hook_state/bcos-framework-issues.jsonl via the Step 7c writer pattern. Write a diary entry dispatcher.error with the validation failure. Report to the user: "schedule-config.json is malformed — fix before the dispatcher can run. Use the schedule-tune skill to repair it." Note: the chat-echo follows the error scenario template from Step 7.3, not the normal compact card.
Schema-conformance nudge (schema 1.2.0+): After successful parse, check the config against the schema 1.2.0 baseline:
- If
digest.auto_commitisfalse→ emit adispatcher-auto-commit-disabledframework finding (yellow, info-level —finding_attrs = {config_path: ".claude/quality/schedule-config.json"}). Surface in the daily digest's framework-issues block. The dispatcher continues —falseis a valid override, just not the recommended default. Forcing function for plugin-package-checklist L7.10e adoption. - If any
jobs.*entry has nooutputs:key → record the job name for the silence-preserving check in Step 7b (do NOT emit a finding here —job-missing-outputs-declarationis emitted later only when the missing declaration actually mattered this tick).
Step 2: Determine Today's Jobs
For each entry in jobs, decide if it should run today. Compare schedule against the current local date:
schedule value | Runs when |
|---|---|
"daily" | Every day |
"mon".."sun" | Only on that weekday |
"weekdays" | Monday through Friday |
"weekends" | Saturday and Sunday |
"1st", "15th" | Only on that day-of-month |
"last" | Last day of the current month |
"every-Nd" | If N or more days since last successful diary run |
| A raw cron string | If today matches the cron's DOW and DOM fields |
"off" | Never |
If enabled is false, skip regardless of schedule.
For on-demand dispatch (user said "run the audit-inbox job"): skip this step, run only the named job.
Build an ordered list of jobs to run. Order: index-health first (always, if enabled), then the rest by config order. Put architecture-review last.
Step 2.5: Refresh the Context Index (once per run)
Before running any job, regenerate .claude/quality/context-index.json exactly once:
python .claude/scripts/context_index.py --write
Every job downstream that needs frontmatter / zone / cluster facets must call load_context_index_cached() from context_index.py rather than re-walking docs/. The cache TTL is 10 min (longer than a full dispatcher cycle), so all jobs in this run hit the same in-memory snapshot. This turns N full-tree walks per dispatcher tick into one — the difference matters for repos with hundreds of docs.
If a job has its own private parser for legacy reasons, that's fine — but new jobs and edits to existing jobs should prefer the cached helper.
Step 3: Read Recent Diary
Read up to the last 30 entries from .claude/hook_state/schedule-diary.jsonl.
Build a per-job summary:
- How many consecutive green runs?
- Findings trend (flat / rising / falling)?
- Last run timestamp per job?
This is used later for frequency-tuning suggestions. Do not change anything based on the diary — suggestions only.
Step 4: Run Each Job
For each job in the list:
-
Load the job reference file from
.claude/skills/schedule-dispatcher/references/job-{name}.md -
Follow the reference's steps (they're self-contained, the dispatcher does not interpret them further)
-
Collect from each job:
verdict: one ofgreen,amber,red, orerrorfindings_count: integerauto_fixed: list of short strings describing fixes appliedactions_needed: list of short strings describing items requiring user judgementnotes: optional free-text (one short paragraph max)
-
Append a diary entry immediately after each job completes (do not batch — if the dispatcher crashes mid-run, we want the partial history). Use the helper script — it creates
.claude/hook_state/on first run and matches the allowlisted command prefix so it never prompts:
python .claude/scripts/append_diary.py '{"ts":"2026-04-15T09:04:12","job":"index-health","verdict":"green","findings_count":0,"auto_fixed":[],"actions_needed":[],"duration_s":4}'
Do NOT use echo ... >> .claude/hook_state/schedule-diary.jsonl — raw redirects into .claude/ trigger the sensitive-file approval prompt on every append.
If a job errors, catch the error, log "verdict":"error" with "notes":"{short error message}", and continue to the next job. Do not stop the dispatcher on one job's failure.
Data-corruption surfacing. JSONL loaders (auto_fix_audit._load_rows, promote_resolutions._load_rows, _load_diary, etc.) use _jsonl_safe.safe_load_jsonl() which records dropped (malformed) lines in a _LAST_LOAD_REPORT module-global. When a job that uses these loaders completes with _LAST_LOAD_REPORT.dropped > 0, surface the report as a data-corruption-detected action item in the digest — the auditor's denominator silently shrinks otherwise. New loaders SHOULD use safe_load_jsonl rather than the legacy try/except: continue pattern; the helper signature returns (rows, report) so callers can include the finding without changing call-site shape.
Wiki jobs are first-class job references and use the same dispatcher contract:
| Job | Reference | Notes |
|---|---|---|
wiki-stale-propagation | references/job-wiki-stale-propagation.md | Daily metadata scan for wiki pages whose builds-on sources changed after last-reviewed. |
wiki-source-refresh | references/job-wiki-source-refresh.md | Weekly two-tier refresh check: HEAD-only quick check at stale_threshold_days/4, full refresh-must-rediscover at stale_threshold_days. |
wiki-graveyard | references/job-wiki-graveyard.md | Monthly stale/orphan/archive-candidate scan. |
wiki-coverage-audit | references/job-wiki-coverage-audit.md | Quarterly cross-zone c |