Skip to main content

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

RiskScripts
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 ERPcb_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 datacb_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 tabevery 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 environmentcb_sync_to_staging.sh — rsync --delete plus a full live→staging DB restore
Nulls stored CRM datacb_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.

ScheduleScriptWhat it does
* * * * *cb_pipedrive_update_deals.phpoutbound quotes → Pipedrive deals
* * * * *cb_pipedrive_process_webhook_queue.phpdrain the inbound webhook queue
*/5 * * * *cb_quote_stage_sync_export_sheet.phpmirror the stage-change log to its sheet
15 6 * * *cb_quote_nudge_build_sheet.phprebuild the "due for a nudge" sheet
0 13 * * *cb_pipedrive_send_cold_quote_prompts.phpemail RSMs about cold deals
0 13 * * *cb_pipedrive_update_cold_quote_sheet.phprebuild the Cold Quotes sheet
30 3 * * *cb_pipedrive_prune_change_log.phpdelete 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.phprefresh 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:

  • flock on 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 -n makes 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.php re-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/

ScriptNotes
cb_bom_cache_update.phpbatched 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.phpresolve an Excel list of items against the cache → <tmp>/bom.csv
cb_pos_bom_recalculate.phprecompute 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.phpemails on BOM fetch-fail reports (counts resolved reports too — docs/_known-issues.md)
cb_infor_coi_codes_normalize_codes.phpwrites Infor — normalizes order-line configuration codes
cb_infor_coi_codes_update_cache.phprefresh the COI code cache + CloudWatch metrics
cb_infor_coi_codes_report_findings.phpfindings → Google Sheet (replaces the tab)
cb_costing_sheet_update_google_sheet.phpMargin Analysis; has a dedicated OOM shutdown handler
cb_run_infor_db_export.phprun one saved SQL export by --id=
cb_update_job_boms.phpjob 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:

LockHeld by
bc_pipedrive_webhook_queue.<dbname>cb_pipedrive_process_webhook_queue.php
bc_quote_stage_sync_exportcb_quote_stage_sync_export_sheet.php
bc_quote_nudge_buildcb_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:

AreaLog types
Pipedrivecustom_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-upcustom_bcquotestagesync, custom_bcquotenudge, bcquotelandingpage
BOM / costingcustom_bom_cache_caching, custom_bom_cache_loading, custom_position_bom_calculation, custom_infor_cpq, custom_costing_sheet
Code normalizationcustom_infor_code_normalization, custom_infor_code_caching
Order linescustom_order_line_status_find_changes, custom_order_line_status_notify
Sheet exportscustom_sheet_exports_* (current generation) and custom_exports_* (older)
Importscustom_converted_lum_code_import, custom_reps_api
Infor connectioncustom_infor_pdo, custom_infor_api_connect_fails
Sheetscustom_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 as cb_sheet_exports_prod_schedule.php (no Old variant exists). The cb_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 by cb_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.sh holds 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.php calls a live cache-purge endpoint with a hardcoded, guessable shared secret.
  • cb_order_line_status_notify.php exits 0 on failure — cron-level alerting will never fire for it.
  • --redo=1 means 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/--id with no isset check, 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 the cb_tmp_* one-offs.
  • Add a lock to the full-replace sheet exports, starting with the costing sheet.
  • Give the five unguarded email senders a --simulate flag, as the cold-quote prompt has.
  • Finish the CLI bootstrap migration the 2026-05-29 commit claimed to have done.