Scheduled jobs & the CLI
Audience: developers & AI agents · Scope:
docroot/cli/— the 67 CLI scripts, what they do, and what runs unattended · Last reviewed: 2026-07-20
TL;DR — Most of the integration work on this site happens unattended: Pipedrive sync, the quote follow-up sheets, the Infor BOM/costing jobs, the schedule exports, order tracking and the metrics pipeline are all cron-driven CLI scripts. The crontab is not in this repo — the server is the source of truth, and only a handful of schedules are written down anywhere. Several scripts are genuinely destructive and a few send real email, so do not run one by hand to "see what it does" without reading its row below.
⚠️ Before you run anything by hand
| Risk | Scripts |
|---|---|
| Sends email to real people (no simulate flag) | cb_send_low_quantity_notifications.php, cb_send_volume_discount_notifications.php, cb_order_line_status_notify.php, cb_pos_bom_send_report_notifications.php, and cb_update_csi_orders.php on its failure path. Only cb_pipedrive_send_cold_quote_prompts.php has --simulate=1. |
| Writes to the production Infor ERP | cb_infor_coi_codes_normalize_codes.php (--redo=1 rewrites 20 years of order lines instead of 24 hours), cb_tmp_update_market_sectors.php (raw batched UPDATE, no dry-run), cb_bom_cache_update.php |
| Deletes or truncates site data | cb_bom_cache_update.php --redo=1 (DELETE with no WHERE), cb_pos_bom_recalculate.php --redo=1 (wipes all position BOM data, not just the date window), configbox_import_converted_lum_codes.php (TRUNCATE), configbox_update_reps.php (delete + reinsert), configbox_gc.php, cb_pipedrive_prune_change_log.php (deletes change-log rows past the retention window — has --dry-run=1) |
| Replaces a whole Google Sheet tab | every cb_sheet_exports_*, cb_update_kitting_picklist.php, cb_costing_sheet_update_google_sheet.php, cb_infor_coi_codes_report_findings.php, cb_pipedrive_update_cold_quote_sheet.php, cb_quote_nudge_build_sheet.php |
| Overwrites an entire environment | cb_sync_to_staging.sh — rsync --delete plus a full live→staging DB restore |
| Nulls stored CRM data | cb_pipedrive_setup_integration.php --replace-data=1, cb_pipedrive_redo_quote_data.php |
The environment is detected from the filesystem path (/var/www/betacalco.com → live,
/var/www/staging.betacalco.com → staging, a path containing /Phpstorm → dev, otherwise unknown). A copy
of the tree in an unexpected location reports unknown and still sends email and still writes to Infor.
How a CLI script is put together
Two generations coexist.
Modern (46 scripts) — via system_overrides/BcCliHelper.php:
require_once dirname(__DIR__).'/components/com_configbox/data/customization/system_overrides/BcCliHelper.php';
BcCliHelper::initJoomlaForCliScriptRun();
class ConfigboxCliThingy extends \Joomla\CMS\Application\CliApplication {
public function getName() { return 'cliThingy'; }
public function doExecute() { /* … */ }
}
BcCliHelper::runCliApp(ConfigboxCliThingy::class);
initJoomlaForCliScriptRun() defines _JEXEC, resolves JPATH_BASE, loads the Joomla framework, aliases the
session services to session.cli, pins Factory::$application, calls initKenedo('com_configbox') — and then
restore_exception_handler(), because Kenedo's handler renders HTML, which is useless in a terminal. It
also fakes HTTP_HOST per environment so URL-building works headlessly — from BcHelper::getSiteHost()
(DDEV_HOSTNAME inside a DDEV container, otherwise by BcHelper::getEnvironmentName(), which honours
BC_ENV: betacalco.ddev.site on dev / staging.betacalco.com / www.betacalco.com).
This is the hostname every link a CLI mints carries — follow-up magic links, cold-quote prompts, emails —
so an environment that resolves wrongly sends people to the wrong site. (Until 2026-07-22 BcCliHelper had
its own stale copy of the environment check that knew neither BC_ENV nor the container path, so every CLI
run inside DDEV minted live-site links.)
runCliApp() wraps execute() in a try/catch, prints the message and trace, and exits 1 on Throwable.
Legacy (21 scripts) duplicate ~35 lines of inline Joomla boilerplate and call initKenedo() inside
doExecute(). All six cb_sheet_exports_*, all nine cb_update_*, configbox_gc.php,
configbox_update_reps.php, configbox_import_converted_lum_codes.php and the two notification senders are
still on it. (A 2026-05-29 commit claims to have converted every script to the namespaced CliApplication; it
did not.)
Writing a new one: use the modern bootstrap, one CliApplication subclass per file, parse arguments with
getopt('', ['name::']), wrap the body in try/catch and re-throw after logging so the exit code is non-zero,
time with KLog::start()/KLog::stop(), and push CloudWatch counters with
AwsMetrics::scheduleMetricDataPut() (deferred) rather than putMetricData() (immediate).
Known schedules
Only these are recorded anywhere in the repo. Everything else is "not recorded" — check the server crontab.
| Schedule | Script | What it does |
|---|---|---|
* * * * * | cb_pipedrive_update_deals.php | outbound quotes → Pipedrive deals |
* * * * * | cb_pipedrive_process_webhook_queue.php | drain the inbound webhook queue |
*/5 * * * * | cb_quote_stage_sync_export_sheet.php | mirror the stage-change log to its sheet |
15 6 * * * | cb_quote_nudge_build_sheet.php | rebuild the "due for a nudge" sheet |
0 13 * * * | cb_pipedrive_send_cold_quote_prompts.php | email RSMs about cold deals |
0 13 * * * | cb_pipedrive_update_cold_quote_sheet.php | rebuild the Cold Quotes sheet |
30 3 * * * | cb_pipedrive_prune_change_log.php | delete change-log entries past the retention window (destructive) |
0 6 * * * (optional) | cb_pipedrive_import_pipelines.php, cb_pipedrive_import_stages.php, cb_pipedrive_import_users.php | refresh pipelines/stages/users lists |
Two more must run frequently even though no schedule is written down:
cb_aws_put_scheduled_metrics.php (every other script only schedules metrics — this is what actually sends
them) and cb_google_sheets_refresh_cache.php.
The crontab (draft — reconcile with the server)
The block below is a proposal, not a transcript of what is running. Only the lines marked [repo] are
schedules this repository actually records (the table above); every line marked [?] is a suggested cadence
inferred from what the script does — diff this against crontab -l on the live box before adopting it,
and correct this file from whatever the server says rather than the other way round.
Two conventions in the draft worth keeping when you reconcile:
flockon every job without its own lock. Only three scripts take a real mutex (see Concurrency); the rest — including every full-replace Google Sheet export — will happily run twice at once.flock -nmakes a contended run exit instead of double-writing a sheet.- Staggered minutes. The daily jobs are spread rather than all landing on the hour, because several are
long (
cb_specsheets_refresh_cache.phpre-exec()s itself once per published product) and share the Infor connection and the Google service account.
# ─── Beta-Calco production crontab ──────────────────────────────────────────────
# Source of truth: docs/scheduled-jobs.md. Keep this file and the server in step.
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
MAILTO= # ← SET THIS. Most scripts report failure only via cron mail.
PHP=/usr/bin/php # ← confirm this is the same PHP the site runs on
CLI=/var/www/betacalco.com/cli # note: deploy rsyncs docroot/ INTO the vhost root, so there is no docroot/ segment
LOCK=/usr/bin/flock
# ─── Pipedrive ↔ quotes ─────────────────────────────────────────────────────────
* * * * * $PHP $CLI/cb_pipedrive_update_deals.php # [repo] outbound quote → deal
* * * * * $PHP $CLI/cb_pipedrive_process_webhook_queue.php # [repo] drain inbound queue (has own lock)
0 13 * * * $PHP $CLI/cb_pipedrive_send_cold_quote_prompts.php # [repo] EMAILS RSMs
10 13 * * * $LOCK -n /tmp/bc_cold_quote_sheet.lock $PHP $CLI/cb_pipedrive_update_cold_quote_sheet.php # [repo]
30 3 * * * $PHP $CLI/cb_pipedrive_prune_change_log.php # [repo] DESTRUCTIVE (retention window)
0 6 * * * $PHP $CLI/cb_pipedrive_import_pipelines.php # [repo] optional; prints stale-default warnings
0 6 * * * $PHP $CLI/cb_pipedrive_import_users.php # [repo] optional; refreshes the local Pipedrive users list (deal-owner lookup)
5 6 * * * $PHP $CLI/cb_pipedrive_import_stages.php # [repo] optional; prints stale-default warnings
# ─── Quote follow-up ────────────────────────────────────────────────────────────
*/5 * * * * $PHP $CLI/cb_quote_stage_sync_export_sheet.php # [repo] mirror stage-change log (has own lock)
15 6 * * * $PHP $CLI/cb_quote_nudge_build_sheet.php # [repo] rebuild "due for a nudge" sheet (has own lock)
20 2 * * * $LOCK -n /tmp/bc_quotes_statuses.lock $PHP $CLI/cb_quotes_update_statuses.php # [?]
35 2 * * * $LOCK -n /tmp/bc_quotes_totals.lock $PHP $CLI/cb_quotes_update_totals.php # [?]
0 14 * * 1-5 $PHP $CLI/cb_send_low_quantity_notifications.php # [?] EMAILS
15 14 * * 1-5 $PHP $CLI/cb_send_volume_discount_notifications.php # [?] EMAILS
# ─── Metrics, probes, caches (frequent; no schedule was ever recorded) ──────────
*/5 * * * * $PHP $CLI/cb_aws_put_scheduled_metrics.php # [?] THE DRAIN — without it all metrics are silently lost
*/5 * * * * $PHP $CLI/cb_ga4_send_scheduled_events.php # [?] queued GA4 events
*/10 * * * * $PHP $CLI/cb_crafty_infor_db_probe.php # [?]
*/10 * * * * $PHP $CLI/cb_crafty_infor_ping_probe.php # [?]
*/10 * * * * $PHP $CLI/cb_aws_send_metric_infordb.php # [?]
*/10 * * * * $PHP $CLI/cb_aws_send_metric_apptrix_vpn.php # [?]
0 * * * * $LOCK -n /tmp/bc_gsheet_cache.lock $PHP $CLI/cb_google_sheets_refresh_cache.php # [?] sheet-picker cache
45 3 * * * $PHP $CLI/cb_aws_prune_metric_log_entries.php # [?] DESTRUCTIVE (log trim)
30 5 * * * $PHP $CLI/cb_bigquery_send_usage_stats.php # [?]
# ─── Infor / BOM / costing ──────────────────────────────────────────────────────
30 1 * * * $LOCK -n /tmp/bc_bom_cache.lock $PHP $CLI/cb_bom_cache_update.php # [?] never pass --redo=1 from cron (wipes the cache)
0 4 * * * $LOCK -n /tmp/bc_coi_cache.lock $PHP $CLI/cb_infor_coi_codes_update_cache.php # [?]
30 4 * * * $LOCK -n /tmp/bc_coi_report.lock $PHP $CLI/cb_infor_coi_codes_report_findings.php # [?] replaces a sheet tab
0 5 * * * $LOCK -n /tmp/bc_costing_sheet.lock $PHP $CLI/cb_costing_sheet_update_google_sheet.php # [?] Margin Analysis
0 15 * * 1-5 $PHP $CLI/cb_pos_bom_send_report_notifications.php # [?] EMAILS fetch-fail reports
#0 2 * * * $PHP $CLI/cb_infor_coi_codes_normalize_codes.php # [?] WRITES TO INFOR — enable deliberately
#0 3 * * * $PHP $CLI/cb_pos_bom_recalculate.php # [?] heavy; usually run by hand for a window
# ─── Schedule exports → Google Sheets ───────────────────────────────────────────
*/30 6-20 * * 1-5 $LOCK -n /tmp/bc_x_prod.lock $PHP $CLI/cb_sheet_exports_prod_schedule.php # [?]
*/30 6-20 * * 1-5 $LOCK -n /tmp/bc_x_fab.lock $PHP $CLI/cb_sheet_exports_fab_schedule.php # [?]
*/30 6-20 * * 1-5 $LOCK -n /tmp/bc_x_fabprio.lock $PHP $CLI/cb_sheet_exports_fab_schedule_priority.php # [?]
*/30 6-20 * * 1-5 $LOCK -n /tmp/bc_x_asm.lock $PHP $CLI/cb_sheet_exports_assembly_schedule.php # [?]
*/30 6-20 * * 1-5 $LOCK -n /tmp/bc_x_eng.lock $PHP $CLI/cb_sheet_exports_engineering_schedule.php # [?]
*/30 6-20 * * 1-5 $LOCK -n /tmp/bc_x_whk.lock $PHP $CLI/cb_sheet_exports_warehouse_kitting_data.php # [?]
*/30 6-20 * * 1-5 $LOCK -n /tmp/bc_x_pick.lock $PHP $CLI/cb_update_kitting_picklist.php # [?] still live despite the cb_update_ name
*/30 6-20 * * 1-5 $LOCK -n /tmp/bc_x_csi.lock $PHP $CLI/cb_update_csi_orders.php # [?] still live; retries once after 120s, then EMAILS
45 1 * * * $LOCK -n /tmp/bc_job_boms.lock $PHP $CLI/cb_update_job_boms.php # [?] still live
# ─── Order tracking ─────────────────────────────────────────────────────────────
20 * * * * $LOCK -n /tmp/bc_ot_po.lock $PHP $CLI/cb_order_tracking_add_po_numbers.php # [?] fills empty cells only
25 * * * * $LOCK -n /tmp/bc_ot_ship.lock $PHP $CLI/cb_order_tracking_add_ship_to.php # [?] fills empty cells only
40 * * * * $PHP $CLI/cb_order_line_status_find_changes.php # [?]
50 * * * * $PHP $CLI/cb_order_line_status_notify.php # [?] EMAILS; exits 0 on failure, so cron alerting never fires
# ─── Housekeeping ───────────────────────────────────────────────────────────────
0 3 * * * $PHP $CLI/configbox_gc.php # [?] expired carts etc.
30 6 * * * $LOCK -n /tmp/bc_reps.lock $PHP $CLI/configbox_update_reps.php # [?] delete+reinsert; aborts if the sheet is empty
0 1 * * 0 $LOCK -n /tmp/bc_specsheets.lock $PHP $CLI/cb_specsheets_refresh_cache.php # [?] LONG (one exec per published product)
0 2 * * 0 $PHP $CLI/cb_browscap_update.php # [?]
# ─── Deliberately NOT scheduled ─────────────────────────────────────────────────
# One-time / manual: cb_pipedrive_setup_integration.php, cb_pipedrive_redo_quote_data.php (destructive
# backfill), configbox_import_converted_lum_codes.php (TRUNCATE + reload),
# cb_products_export_prod_images.php, cb_run_infor_db_export.php (needs --id),
# cb_quotes_get_pdf.php, cb_bom_cache_get_spreadsheet.php (needs --file)
# Test / dev only: cb_pipedrive_test_support.php (writes to REAL Pipedrive unless PIPEDRIVE_TEST=capture),
# cb_dev_make_mvc_files.php, cb_sync_to_staging.sh (live → staging, plaintext creds),
# cb_pipedrive_api_parity.php (read-only v1-vs-v2 check; run by hand during the
# API v2 migration — exits 1 on a difference, so it can gate a rollout phase)
# Dead / superseded: cb_update_production_schedule.php, cb_update_{assembly,engineering,fabrication,
# fabrication_priority,warehouse_kitting_data}*.php (call the …Old() methods),
# cb_check_infor_db.php, cb_aws_add_new_metrics.php (empty scaffold),
# cb_data_duplicate_reused_options.php, cb_tmp_*.php (one-offs; one still points at
# production Infor)
Before adopting: confirm $PHP/$CLI, set MAILTO, and check whether the legacy …Old() schedule
exports are still consumed by anyone — if a team still reads those sheets, they belong in the list rather than
in "dead", and the fix is to retire the sheet, not to quietly keep both generations running.
The inventory
Detail for each area lives in its own doc; this is the map.
Pipedrive / CRM — see pipedrive/
cb_pipedrive_update_deals.php · cb_pipedrive_process_webhook_queue.php ·
cb_pipedrive_import_pipelines.php · cb_pipedrive_import_stages.php · cb_pipedrive_import_users.php ·
cb_pipedrive_send_cold_quote_prompts.php · cb_pipedrive_update_cold_quote_sheet.php ·
cb_pipedrive_setup_integration.php (one-time) · cb_pipedrive_redo_quote_data.php (backfill) ·
cb_pipedrive_prune_change_log.php (destructive — trims the change log to the retention window;
--weeks=N, --dry-run=1) ·
cb_pipedrive_test_support.php (test harness — writes to real Pipedrive unless PIPEDRIVE_TEST=capture)
Quote follow-up — see quote-follow-up/
cb_quote_nudge_build_sheet.php · cb_quote_stage_sync_export_sheet.php ·
cb_quotes_update_statuses.php · cb_quotes_update_totals.php · cb_quotes_get_pdf.php ·
cb_send_low_quantity_notifications.php · cb_send_volume_discount_notifications.php
Infor / ERP — see infor/
| Script | Notes |
|---|---|
cb_bom_cache_update.php | batched BOM cache refresh; signal handlers clear the in-progress flag. --redo=1 deletes the whole cache table and nulls Uf_BCDateLastBomCalc on every row in production Infor — see bom/bom-cache.md |
cb_bom_cache_get_spreadsheet.php | resolve an Excel list of items against the cache → <tmp>/bom.csv |
cb_pos_bom_recalculate.php | recompute quote-position BOMs for a date/status window (--start-date, --end-date, --status all required) — see bom/position-boms.md |
cb_pos_bom_send_report_notifications.php | emails on BOM fetch-fail reports (counts resolved reports too — docs/_known-issues.md) |
cb_infor_coi_codes_normalize_codes.php | writes Infor — normalizes order-line configuration codes |
cb_infor_coi_codes_update_cache.php | refresh the COI code cache + CloudWatch metrics |
cb_infor_coi_codes_report_findings.php | findings → Google Sheet (replaces the tab) |
cb_costing_sheet_update_google_sheet.php | Margin Analysis; has a dedicated OOM shutdown handler |
cb_run_infor_db_export.php | run one saved SQL export by --id= |
cb_update_job_boms.php | job BOMs from a sheet |
Schedule exports → Google Sheets — see google-sheets/consumers.md
cb_sheet_exports_prod_schedule.php · cb_sheet_exports_fab_schedule.php ·
cb_sheet_exports_fab_schedule_priority.php · cb_sheet_exports_assembly_schedule.php ·
cb_sheet_exports_engineering_schedule.php · cb_sheet_exports_warehouse_kitting_data.php ·
cb_update_kitting_picklist.php (still live) · cb_update_csi_orders.php (still live; retries once after
120 s, then emails a hardcoded recipient list)
Order tracking
cb_order_tracking_add_po_numbers.php · cb_order_tracking_add_ship_to.php (both fill empty cells only) ·
cb_order_line_status_find_changes.php · cb_order_line_status_notify.php (emails; swallows exceptions
and exits 0, so cron alerting never sees a failure)
Imports
configbox_update_reps.php (Where-to-Buy reps; delete + reinsert in a transaction, aborts if the sheet is
empty) · configbox_import_converted_lum_codes.php (TRUNCATE + reload) ·
cb_products_export_prod_images.php (import/export product images; import overwrites)
There is no CLI for the price-list import — that is admin-UI only.
Metrics & probes
cb_aws_put_scheduled_metrics.php (the drain — everything else only queues) ·
cb_aws_prune_metric_log_entries.php · cb_aws_send_metric_infordb.php ·
cb_aws_send_metric_apptrix_vpn.php · cb_aws_add_new_metrics.php (a no-op scaffold as committed) ·
cb_bigquery_send_usage_stats.php · cb_ga4_send_scheduled_events.php ·
cb_check_infor_db.php / cb_crafty_infor_db_probe.php / cb_crafty_infor_ping_probe.php
CloudWatch namespaces used from the CLI: Infor-DB-Checks, BOM-Cache, BigQuery-Usage,
Infor-Order-Notifications, Margin-Analysis-Tool, Specsheet-Cache-Refresh.
Caches & housekeeping
configbox_gc.php (expired carts etc.) · cb_google_sheets_refresh_cache.php ·
cb_specsheets_refresh_cache.php (without --product-id it re-exec()s itself once per published
product) · cb_browscap_update.php · cb_data_duplicate_reused_options.php (completed one-off)
Dev & infrastructure
cb_dev_make_mvc_files.php (MVC scaffolder) · cb_sync_to_staging.sh (live → staging, see the warning below)
· joomla.php (Joomla core, not ours)
Concurrency
Only three scripts take a real mutex — a MySQL advisory lock with GET_LOCK(…, 0), so a contended run is a
clean no-op, released in a finally:
| Lock | Held by |
|---|---|
bc_pipedrive_webhook_queue.<dbname> | cb_pipedrive_process_webhook_queue.php |
bc_quote_stage_sync_export | cb_quote_stage_sync_export_sheet.php |
bc_quote_nudge_build | cb_quote_nudge_build_sheet.php |
Only the webhook-queue lock is namespaced by database name, so it is the only one safe against two schemas sharing a MySQL server.
The BOM cache uses a weaker guard — a DB flag rather than a lock, with --ignore-in-progress-flag=1 to bypass
it; cb_bom_cache_get_spreadsheet.php and cb_pos_bom_recalculate.php cooperate by polling for up to five
minutes. A hard-killed refresh leaves the flag set and blocks every later run until it is bypassed — the usual
reason the cache "stops updating". See bom/bom-cache.md.
Everything else has no concurrency guard at all — including every full-replace Google Sheet export. Two overlapping runs interleave their writes against the same tab. The costing sheet is the most exposed, being slow and memory-hungry enough to warrant its own OOM handler.
Logging
Every area writes its own KLog type; the useful ones when debugging a job:
| Area | Log types |
|---|---|
| Pipedrive | custom_pipedrive_sync_quote_data, custom_pipedrive_cold_quote_prompts, custom_pipedrive_import_stages, custom_pipedrive_import_pipelines, custom_pipedrive_setup, custom_pipedrive_redo_quote_data, custom_pipedrive_exports |
| Quote follow-up | custom_bcquotestagesync, custom_bcquotenudge, bcquotelandingpage |
| BOM / costing | custom_bom_cache_caching, custom_bom_cache_loading, custom_position_bom_calculation, custom_infor_cpq, custom_costing_sheet |
| Code normalization | custom_infor_code_normalization, custom_infor_code_caching |
| Order lines | custom_order_line_status_find_changes, custom_order_line_status_notify |
| Sheet exports | custom_sheet_exports_* (current generation) and custom_exports_* (older) |
| Imports | custom_converted_lum_code_import, custom_reps_api |
| Infor connection | custom_infor_pdo, custom_infor_api_connect_fails |
| Sheets | custom_google_sheet_cache, custom_sql_exports |
Watch for the inconsistency: cb_sheet_exports_assembly_schedule.php writes to the old
custom_exports_assembly_schedule type while its five siblings use custom_sheet_exports_*.
Dead or superseded
cb_update_production_schedule.php— calls exactly the same method ascb_sheet_exports_prod_schedule.php(noOldvariant exists). Thecb_sheet_exports_one is the documented cron, so this is the dead copy.cb_update_assembly_schedule.php,cb_update_engineering_schedule.php,cb_update_fabrication_schedule.php,cb_update_fabrication_schedule_priority.php,cb_update_warehouse_kitting_data.php— all call superseded…Old()methods.cb_check_infor_db.php— superseded bycb_crafty_infor_db_probe.php.cb_tmp_check_cold_quotes.php,cb_tmp_test_pd_import.php,cb_tmp_update_market_sectors.php— one-offs; the last still points at production Infor.cb_data_duplicate_reused_options.php— completed migration.cb_aws_add_new_metrics.php— an intentionally empty scaffold.
Gotchas & caveats
cb_sync_to_staging.shholds live and staging MySQL credentials in plaintext, committed to this repo. They should be moved to a gitignored config file and rotated — they are in git history, so removing them from the working tree is not sufficient. Treat as compromised until rotated.cb_products_export_prod_images.phpcalls a live cache-purge endpoint with a hardcoded, guessable shared secret.cb_order_line_status_notify.phpexits 0 on failure — cron-level alerting will never fire for it.--redo=1means very different things across scripts: a wider time window (cb_infor_coi_codes_normalize_codes.php), or a full table wipe (cb_bom_cache_update.php,cb_pos_bom_recalculate.php). Read the row before using it.- Several scripts read an optional
--file/--idwith noissetcheck, so omitting it produces a PHP warning and a confusing downstream failure rather than a usage message.
Possible follow-ups
- Get the crontab into the repo (or at least into this doc). Today the schedule of ~60 jobs exists only on the server, so nobody can tell from the code what runs, how often, or whether a job has silently stopped.
- Retire the superseded
cb_update_*scripts and thecb_tmp_*one-offs. - Add a lock to the full-replace sheet exports, starting with the costing sheet.
- Give the five unguarded email senders a
--simulateflag, as the cold-quote prompt has. - Finish the CLI bootstrap migration the 2026-05-29 commit claimed to have done.