Repository source: docs/reference/v3_cli.md
v3 CLI Reference
./v3 is a thin dispatcher over the PHP scripts in scripts/. Every subcommand
below can also be invoked directly as php scripts/<script>.php ...; the ./v3
form is the shorthand used elsewhere in this repo's docs.
Run ./v3 with no arguments to print the same command list from the script
itself (useful if this document drifts from v3).
CLI error-handling contract
When adding or changing a ./v3 command or worker command:
- validate the command and its options before filesystem, database, network, or
other state-changing work begins;
- accept only documented options and reject unknown commands or options with a
nonzero exit code;
- write a concise error and the relevant usage text to stderr—never expose an
uncaught PHP exception or stack trace;
- support
-hand--helpwith exit code 0; and - add a regression test through the
./v3dispatcher for unknown-option
handling.
Most data-touching commands accept optional positional repository_root and
database_path arguments. When omitted they fall back to, in order:
- the relevant
FORUM_*environment variable (FORUM_REPOSITORY_ROOT,
FORUM_DATABASE_PATH, FORUM_STATIC_HTML_ROOT)
- the default local repository/database bootstrapped under
state/
See Local Run in the README for the defaults and the bootstrap flow.
Synchronize the Feature Development Process documentation
./v3 fdp sync [--repository-root=/path/repository] [--remote-url=https://github.com/gulkily/fdp.git]
Updates the vendored docs/fdp/ subtree from gulkily/fdp's main branch and
commits the resulting documentation update. Run it from a clean target working
tree. By default it updates this checkout; use --repository-root to update
another v3 copy from the same command. The command adds the fdp remote when
missing and otherwise preserves its configured URL. Pass --remote-url only to
intentionally set that remote, such as when testing a fork.
The command also repairs legacy FDP imports whose recorded upstream commit was rewritten, so later syncs use the ordinary subtree pull workflow.
Start the local dev server
./v3 start [--listen-all|host:port]
Starts the PHP built-in dev server on 127.0.0.1:8000 by default.
--listen-all— bind0.0.0.0:8000instead of127.0.0.1:8000host:port— bind an explicit address or bare port number instead of the default
Run the test suite
./v3 test
Runs the custom test runner (tests/run.php) covering parser, rebuild, and
app smoke tests.
- any arguments — passed straight through to the test runner, e.g. a specific test class name
Check the live OpenPGP asset contract
./v3 openpgp smoke [--origin=zenmemes.com]
Performs a read-only live check of the HTTP and HTTPS browser runtime paths.
For each scheme it reads window.__forumAssetPaths from the public page, selects
the same OpenPGP v5 (HTTP) or v6 (HTTPS) asset the browser would use, then also
checks both legacy raw OpenPGP bundle URLs used by cached clients. It reports
the selected URL, status, content type, redirects, and a failure reason. The
default host is zenmemes.com; use --origin=staging.example to check a staging
host. No records, browser identities, or posts are created.
Run the manual OpenPGP production canary
npm install
./v3 openpgp canary --confirm-production-write --browser-executable=/path/to/chromium
This is deliberately not a CI or deployment command. After the read-only
smoke passes, manually run the canary from a machine with Node 18+ and local
Chromium. It uses which chromium automatically; pass
--browser-executable=/path/to/chromium only when that lookup is unsuitable.
It opens an isolated HTTP browser profile, creates a new identity using the
consistent username release-check, publishes “New release just dropped,
making sure it works,” reloads the post, and exercises the existing browser identity path to
confirm no second username prompt appears. It leaves the identity and post in
place intentionally. Do not rerun it after an ambiguous result—first inspect
the reported post URL. --confirm-production-write is mandatory.
Inspect operator status
./v3 status [--repository-root=/path/repository] [--database-path=/path/read-model.sqlite3] [--queue-database-path=/private/path/tasks.sqlite3]
Read-only, concise status for the repository, read model, shared execution
lock, and background queue. It reports a read-model rebuild task as queued,
running, failed, or absent, then prints the relevant next action.
--repository-root=...— canonical repository to compare with read-model metadata--database-path=...— read-model SQLite database to inspect--queue-database-path=...— task-queue SQLite database to inspect
The command remains useful when optional runtime files are missing or
unreadable: it reports the affected state without creating a queue, lock, or
read-model database. A Shared lock: locked result only means protected
activity is in progress; it does not prove a manual rebuild is running.
Use ./v3 task-queue status for individual task details and
./v3 fast-score status for Fastmod work details.
Rebuild the SQLite read model
./v3 rebuild [repository_root] [database_path]
./v3 rebuild diagnose [repository_root] [database_path]
./v3 rebuild recover --confirm [repository_root] [database_path]
Rebuilds the SQLite read model from canonical records in the repository root. Reports source scanning, candidate construction and validation, lock wait, and promotion phases. During candidate construction, it emits read-model stages and bounded record-parsing progress checkpoints. It also prints source record counts (posts, identities, approval seeds) and resulting read-model table counts (posts, threads, profiles, activity).
diagnose is read-only: it reports SQLite sidecars, the application rebuild
lock, and Linux /proc file holders. If a stopped process left sidecars behind,
recover --confirm snapshots and archives the database and all sidecars together
under state/cache/read-model-recovery-*, then rebuilds a fresh derived model.
It refuses recovery while the lock or an open file holder is detected.
repository_root— canonical records checkout to rebuild fromdatabase_path— SQLite file to write the read model to
Manage the background task queue
./v3 task-queue enqueue-rebuild [--queue-database-path=/private/path/tasks.sqlite3]
./v3 task-queue enqueue-fast-score [--queue-database-path=/private/path/tasks.sqlite3]
./v3 task-queue enqueue-offline-snapshot [--queue-database-path=/private/path/tasks.sqlite3]
./v3 task-queue run [--limit=1] [--score-limit=25] [--work-limit=250] [--dry-run] [--quiet] [--verbose] [--repository-root=/path/repository] [--database-path=/path/read-model.sqlite3] [--queue-database-path=/private/path/tasks.sqlite3]
./v3 task-queue status [--limit=25] [--queue-database-path=/private/path/tasks.sqlite3]
./v3 task-queue cron [--log=/var/log/forum-task-queue.log]
A small SQLite-backed job queue (scripts/task_queue.php), currently used to
serialize read-model rebuild requests and Fastmod sweeps so concurrent
triggers coalesce into one job instead of racing. docs/runbooks/production_deploy.md and
docs/runbooks/operator_recovery.md reference this command and depend on it
being installed via cron.
enqueue-rebuild— enqueues aread-modelrebuild task (a no-op if one is
already queued/running); --queue-database-path=... overrides the default
queue database location
enqueue-fast-score— enqueues the coalesced Fastmod sweep (also a
no-op if one is already queued/running). It evaluates all nonempty posts over successive bounded runs when Fastmod is enabled.
enqueue-offline-snapshot— enqueues the coalesced public offline snapshot
publication (also a no-op if one is already queued/running).
run— claims and runs up to--limitqueued tasks (default 1), recovering
any abandoned in-progress tasks first; guarded by an exclusive file lock so
concurrent invocations don't double-run. --dry-run reports the queued
count without running anything; --quiet suppresses progress output;
--repository-root=.../--database-path=... override the read model to
rebuild against. --score-limit=... independently sets the maximum provider
calls a claimed Fastmod sweep may make (default 25); provider failures and
invalid structured responses count because a request was made.
--work-limit=... bounds all examined Fastmod work rows, including local
heuristic exclusions (default 250). --limit=... is only the maximum queue
tasks claimed. --verbose reports each Fastmod result and provider request
as it happens. --quiet suppresses all worker progress output, including
verbose output when both options are supplied.
status— prints queued/running/completed/failed counts plus the
--limit (default 25) most recent tasks with attempt counts and failure
codes
cron— prints one ready-to-install crontab line running the worker once a
minute. --log=... sets the log file path baked into the line.
Audit or backfill Fastmod
./v3 fast-score status [--limit=10] [--verbose]
./v3 fast-score audit --include-existing [--database-path=/path/read-model.sqlite3]
./v3 fast-score backfill --include-existing --confirm --max-posts=N --max-cost-usd=N [--database-path=/path/read-model.sqlite3]
./v3 fast-score retry --post-id=... --content-hash=... --rubric-revision=...
./v3 fast-score invalidate --post-id=... --content-hash=... --rubric-revision=...
./v3 fast-score smoke --post-id=... [--database-path=/path/read-model.sqlite3]
./v3 fast-score prune [--before=ISO-8601]
audit --include-existing is a read-only historical count and configured-model
cost estimate. backfill requires separate historical scope, confirmation,
post-count, and spend bounds; it creates one private, bounded batch and queues
the normal worker. status provides the next action, distinguishes regular
work from historical backfill, and shows batch progress and reservation state.
Use --verbose for recent individual work rows. See
Fastmod for pricing configuration, retention, and the
controlled operator workflow.
Import a repository archive
./v3 import-repository <archive.tar.gz> [repository_root] [database_path] [artifact_root] [--dry-run] [--no-commit]
Imports a .tar.gz repository archive into the target repository root,
rebuilds the read model, and rebuilds static artifacts (when an artifact root
is resolved). The command reports each phase (validation, extraction, record
indexing, staging, commit, and derived rebuilds) and emits bounded record-file
progress checkpoints while it processes the archive.
archive.tar.gz— required path to the archive to importrepository_root— canonical records checkout to import intodatabase_path— SQLite file to rebuild after importartifact_root— static HTML output directory to rebuild after import--dry-run— validate the archive without writing anything--no-commit— skip the git commit step after import
Inspect a thread's attributes and labels
./v3 thread-attributes <thread_id_or_record> [repository_root] [database_path]
Diagnostic/read-only. Prints the target item, canonical root post attributes, derived read-model thread attributes, effective labels, and each thread-label record that contributes a label.
thread_id_or_record— a thread ID, post ID, canonical record path, or an unambiguous canonical record filename stemrepository_root— canonical records checkout to read fromdatabase_path— read-model SQLite file to read from
Delete a canonical record
./v3 delete-record <record_path_or_id> [repository_root] [database_path] [artifact_root]
Deletes a canonical file under records/ with git rm, commits the
removal, rebuilds the read model, and rebuilds static artifacts when an
artifact root is resolved (via argument or FORUM_PUBLIC_ARTIFACT_ROOT).
record_path_or_id— a relative path (e.g.records/thread-labels/thread-label-20260530000001-zenrules.txt) or an unambiguous filename stem (e.g.thread-label-20260530000001-zenrules)repository_root— canonical records checkout to delete fromdatabase_path— SQLite file to rebuild after deletionartifact_root— static HTML output directory to rebuild after deletion
Backfill Unicode-risk analysis for posts
./v3 unicode-risk-backfill [repository_root] [database_path] [--with-llm]
Backfills Unicode-risk analysis for existing posts. By default uses deterministic detection only.
repository_root— canonical records checkout to analyzedatabase_path— SQLite file to read posts from and write results to--with-llm— also run the configured LLM provider, not just deterministic detection--deterministic-only— explicitly disable LLM use (useful after--with-llmearlier in the same invocation)
Build static HTML artifacts
./v3 build-static [repository_root] [database_path] [artifact_root]
Builds and atomically activates a complete static HTML release. It creates the candidate read model and HTML release before replacing either live pointer, so normal requests continue using the prior complete state while the command runs.
repository_root— canonical records checkout to render fromdatabase_path— read-model SQLite file to render fromartifact_root— static release root; defaults tostate/static_htmlwhen not given via argument orFORUM_STATIC_HTML_ROOT. The active release isartifact_root/current.
Refresh only shared static pages
./v3 build-static --shared-only [repository_root] [database_path] [artifact_root]
Quickly creates and activates a release by copying the active complete release
and rerendering only shared routes (Board, Activity, Users, Tools, and similar
pages), including the service worker and its referenced assets. It does not
rebuild the read model or render tag, thread, post, or profile pages. Use it
after deploying presentation or fingerprinted-asset changes when the active
release already exists. It preserves the existing detail pages, so use the
full build-static after content changes or changes to a detail-page template.
Archive and remove a thread
./v3 archive-thread <thread_id> [repository_root] [database_path] [artifact_root] [archive_path]
Archives a full thread (root post, replies, supporting public keys) into a
.tar.gz archive, removes the component records from the live repository,
and rebuilds affected artifacts.
thread_id— required ID of the thread to archiverepository_root— canonical records checkout to archive from and remove records fromdatabase_path— SQLite file to rebuild after archivingartifact_root— static HTML output directory to rebuild after archivingarchive_path— output path for the.tar.gz; defaults to a generated path under the project's archive directory
Manage the private LLM config
./v3 private-config [view|edit|refresh-template|--view|--edit|--force|--api-key-stdin] [--path=/private/path/secrets.php]
Creates or updates the private PHP config consumed by
ForumRewrite\Support\PrivateConfig (LLM provider, API key, and related
settings for agent-reply features). LLM_PROVIDER is required (no default);
supported values are openai, openrouter, anthropic, stub, and
OpenAI-compatible gateways. Legacy DEDALUS_* settings are still read as
fallbacks, but new writes use LLM_* names.
view/--view— print a redacted summary and update reminders without writing the fileedit/--edit— open an existing config in$VISUAL,$EDITOR, orvi; it does not print secrets or create a missing filerefresh-template— rewrite the file with current comments/examples while preserving existing values--force— overwrite without the usual confirmation/skip behavior--api-key-stdin— read the API key from stdin instead of an argument, so it never lands in shell history:printf '%s\n' "$LLM_API_KEY" | ./v3 private-config --api-key-stdin--path=...— write to a specific file instead of the default../forum-private/secrets.php(relative to this checkout)
Print the agent-reply cron install reference
./v3 agent-reply cron [--log=/var/log/forum-agent-replies.log]
Prints a ready-to-install crontab line (running
scripts/run_agent_reply_requests.php --quiet --limit=10 once a minute) plus
pre/post-install checks to run and the log file to tail. This is a reference
printer, not the worker itself — see "Run the queued agent-reply worker"
below for that.
--log=...— log file path to bake into the printed crontab line
Run the queued agent-reply worker
./v3 agent-reply cron run [--limit=10] [--dry-run] [--quiet] [--post-id=<id>]
Runs the queued agent-reply worker directly (what the cron line above invokes).
--limit=...— maximum number of queued requests to process--dry-run— report the queued request count without generating replies--quiet— suppress progress output--post-id=...— restrict processing to one post
Show agent-reply diagnostics
./v3 agent-reply status [post_id] [--limit=25] [--database-path=/path/post_index.sqlite3]
Read-only diagnostics for skipped or failed agent-reply generation rows.
post_id— when given, shows that post's rows (limit defaults to 10, capped at 100); when omitted, shows the most recent skipped rows (limit defaults to 25)--limit=...— maximum number of rows to show--database-path=...— read-model SQLite file to read from
Test the live LLM provider connection
./v3 agent-reply test [--timeout=30]
Sends one live structured prompt to the configured LLM provider to validate the API key and model/service reachability.
--timeout=...— seconds to wait for the provider response
Run the local agent-reply test suite
./v3 agent-reply test-local
Runs the local (non-live) agent-reply test suite: AgentReplyGenerationTest,
AgentReplyCommandTest, and targeted LocalAppSmokeTest /
WriteApiSmokeTest cases covering the cron reference command, status
command, and queued-request processing.
- no parameters
Run approved Codex handoff requests
./v3 codex-handoff run [--limit=1] [--dry-run] [--database-path=/path/post_index.sqlite3] [--codex-bin=/path/codex]
Runs approved Codex handoff requests through the local codex binary.
--limit=...— maximum number of handoffs to run--dry-run— report the approved-handoff count without running anything--database-path=...— read-model SQLite file to read handoffs from--codex-bin=...— path to thecodexexecutable; also overridable viaFORUM_CODEX_EXECUTABLE
Run the local Codex handoff test suite
./v3 codex-handoff test-local
Runs the local Codex handoff test suite: CodexHandoffDraftServiceTest,
CodexHandoffStoreTest, CodexHandoffRunnerTest, and the WriteApiSmokeTest
cases covering the handoff API, approval/rejection flow, development-tag
requirements, activity lifecycle, and UI bindings.
- no parameters
Seed an approved identity (shorthand)
./v3 approve <identity_id> [seed_reason] [repository_root] [database_path]
Shorthand alias for ./v3 approval seed (see below for parameter details).
Seed an approved identity
./v3 approval seed <identity_id> [seed_reason] [repository_root] [database_path]
Seeds an initial approved identity (e.g. the first admin) so
FORUM_APPROVED_MEMBERS_ONLY can be enabled without a chicken-and-egg
approval problem.
identity_id— required identity to seed as approvedseed_reason— free-text reason recorded with the seed; defaults to"initial approved user"repository_root— canonical records checkout to write the approval seed todatabase_path— SQLite file to rebuild after seeding
Approve an identity
./v3 approval approve <approver_identity_id> <target_identity_id> [repository_root] [database_path] [artifact_root]
Records an approval of target_identity_id by an already-approved
approver_identity_id, and rebuilds affected artifacts when an artifact root
is resolved.
approver_identity_id— required, must already be an approved identitytarget_identity_id— required identity being approvedrepository_root— canonical records checkout to write the approval todatabase_path— SQLite file to rebuild after approvingartifact_root— static HTML output directory to rebuild after approving
Standalone scripts (not wired into ./v3)
These live in scripts/ but have no ./v3 dispatcher entry — invoke them
directly with php scripts/<script>.php.
Audit post signatures
php scripts/audit_post_signatures.php [repository_root]
Scans canonical post records and reports counts for signed_valid,
missing_signature, invalid_signature, unknown_author_key, and
anonymous_unsigned.
repository_root— canonical records checkout to audit; defaults toFORUM_REPOSITORY_ROOTor the bootstrapped local repository
Build the SQLite query catalog
php scripts/build_sqlite_query_catalog.php [source_directory] [browser_asset_path] [local_pack_path]
Regenerates the SQLite viewer's preset query catalog from queries/sqlite/:
rewrites the generated block inside the browser viewer asset
(public/assets/sqlite_viewer.js) and writes a local .sql query pack.
Run this after adding or editing a query under queries/sqlite/.
source_directory— query source directory; defaults toqueries/sqlitebrowser_asset_path— viewer JS asset to rewrite the generated block in; defaults topublic/assets/sqlite_viewer.jslocal_pack_path— output path for the local.sqlquery pack; defaults topublic/assets/sqlite_query_catalog.sql
Check static artifacts for missing fingerprinted assets
php scripts/check_static_artifacts.php [artifact_root]
Scans every .html file under artifact_root for fingerprinted asset
references (/assets/name.<12-hex>.ext) and fails (exit 1, listing each
missing reference) if any referenced asset file doesn't exist. Useful after
a static build or asset-fingerprint change to catch broken references
before they ship.
artifact_root— directory to scan; defaults toFORUM_PUBLIC_ARTIFACT_ROOTorpublic/