Skip to content

Local dogfood scheduling

praetorctl dogfood schedule run --config /absolute/schedule.json executes at most one due suite. status reads current admission state without creating files or executing cases. A workstation timer can invoke run periodically; the CLI itself is finite and requires Linux with procfs and advisory file locking.

Create a private JSON configuration (0600) with clean absolute paths:

{
  "version": 1,
  "runner_binary": "/home/example/.local/bin/praetorctl",
  "suite_config": "/home/example/.config/praetor/dogfood-suite.json",
  "source_root": "/home/example/dev/praetor",
  "state_dir": "/home/example/.local/state/praetor/dogfood",
  "allow_remote": true,
  "interval_seconds": 86400,
  "retry_seconds": 3600,
  "max_consecutive_failures": 3,
  "max_runs": 32,
  "max_bytes": 2147483648,
  "retention": "block"
}

version, runner_binary, suite_config, source_root, state_dir, and allow_remote are required. Numeric limits and retention may be omitted to use the illustrated defaults. Nulls, duplicate fields (case variants included), unknown fields, incorrect types, relative paths and symlink inputs are rejected. The suite configuration must also be private. state_dir is created with mode 0700 under an existing parent. Existing state metadata must be private; updating a read-only state file is refused instead of broadening its permissions.

Limits are 60..2592000 seconds between unchanged successful runs, 60..86400 seconds between retries, 1..3 consecutive failures, 1..1024 retained attempts, and 1 MiB..16 GiB retained bytes. Optional retention is either "block" (default) or "prune-oldest". The byte limit is an admission threshold, not an in-flight disk quota: one running suite may cross it. Accounting includes all entries, hidden files, Git objects, caches, input snapshots and partial attempts, stopping safely at a hard bound of 4 x the entry cap (800000 entries). In block mode, exceeding max_runs, max_bytes, or 200000 entries returns status resource_blocked (with the entry limit reason recorded in the report), and nothing is deleted. In prune-oldest mode, before admission, while retained runs exceed max_runs, entries exceed 100000 (half of the entry cap), or bytes reach max_bytes, the scheduler deletes the oldest retained run directory inside state_dir (never the newest attempt and never an attempt with running status). The durable state persists pruned_through with fsync before each directory deletion, and the report lists pruned_runs. Retained run directories run-000001..run-N must exist gapless from pruned_through + 1 through attempts. A directory left at or below pruned_through by a process that stopped between saving the state and deleting is removed on the next tick and listed in pruned_runs, so an interrupted prune never stops the schedule (TestSchedulePruneOldestResumesAnInterruptedPrune). Dry runs (status) report what would be pruned and delete nothing.

The input fingerprint binds the exact schedule and suite bytes, configured CLI binary content, source manifest and lock, every file under .config/archetypes (including facets), and optional repair routing/usage snapshots. The complete bundle snapshot is bounded to 512 entries and 16 MiB. A run hashes its actual /proc/self/exe descriptor and requires it to match runner_binary; replacing the installed pathname cannot misidentify already running code. Status uses the configured runner, so an MCP server and CLI report the same fingerprint. Inputs are copied into each attempt before suite execution. Each declared transcript is copied once, after its SHA-256 is verified, to state_dir/transcripts/<id>-<sha256>/<source-base-name>. Both store directories are 0700, and the transcript file is 0600. Keeping the source base name means Antigravity inputs remain transcript.jsonl or transcript_full.jsonl, while a Claude Code input retains its session UUID file name. Every attempt's inputs/suite.json points at that stored file, and replay reports the same path that the reader opened. Later runs therefore do not depend on a coding client's original transcript remaining available. A source base name must be one safe file-name component of at most 255 bytes. An existing stored file is verified before use and is never silently replaced. The transcript store holds pinned inputs, not run evidence, and prune-oldest never removes it. Transcript hashes and public commit pins remain explicit and are never updated.

A successful unchanged input waits interval_seconds after completion. Failures wait retry_seconds; three failures for unchanged inputs open the circuit. Changed engine or configuration inputs reset the failure circuit only when a new run is admitted, after at least retry_seconds since the previous start. A clock rollback delays admission. No reset or force-execution command exists.

A due run waits for the race stage's host admission policy: memory and swap bounds are capped to the host's size, and measured one-minute load must be at most 1.25 times its CPU count (admission.HostCappedPolicy). A held tick returns host_held with host_reason; the next tick retries, with no attempt or failure counted. Status-only reads stay due without reading the host. A host read error stops the tick.

An advisory lock prevents overlapping invocations and is released by the kernel when a process exits. A tick that finishes unlocks it before closing it, so a subprocess still holding a copy of the descriptor cannot leave the next tick busy (LockExclusive in internal/util/file_lock_unix.go). The lock file is retained. Before executing the suite the scheduler saves and fsyncs the attempt's running state and provisional failure count. An abandoned running attempt is recorded as interrupted on the next tick; its partial evidence remains. Corrupt or inconsistent state, missing attempt sequences, failed snapshot writes and failed persistence all stop execution.

Output distinguishes due, cooldown, busy, circuit_blocked, resource_blocked, host_held, verified, and failed. last_attempt.status preserves a previous failure or interruption even during cooldown. Only a newly completed successful run sets top-level verified: true; status never claims new verification. Read-only status and admission no-ops exit successfully with their explicit status; execution and metadata errors return nonzero.

An optional repair_policy enables local failure triage plans:

{
  "routing_config": "/home/example/dev/praetor/.config/models/routing.yaml",
  "task": "ci_debugging",
  "input_tokens": 8000,
  "output_tokens": 2000,
  "max_cost": 0.10
}

The object is the value of repair_policy, with optional usage_path for an explicit capacity observation. routing_config, task, input_tokens, output_tokens and max_cost are required; an empty or partial object is rejected. register, register_source, max_output_tokens, prompt_register, prompt_register_source and register_manifest_sha256 are accepted but ignored: RunSchedule and ScheduleStatus replace the whole register tuple from the current task and prompt resolutions in the captured source manifest (source_root/.standards.yaml) — the same canonicalization dogfood repairs applies (CanonicalRepairPolicy in internal/dogfood/repair.go; TestScheduleCanonicalizesCallerRepairRegisterPolicy in internal/dogfood/schedule_test.go). There are no register values to copy from praetorctl dogfood repairs: its standard output reports only status, job count, estimated cost and report fingerprint (see local repair plans). Its files are snapshotted with the suite. Actual failed suite reports produce plans under run-000001/repairs; nil reports and prevalidation errors cannot invent repair jobs. Planning uses at most one extra minute of bookkeeping even after suite cancellation; cancellation cannot silently discard a completed failure report. Planning or persistence errors preserve the original suite failure. Green runs skip repair planning. See local repair plans for configured-cost and capacity limits. This stage does not call providers, launch coding agents, commit fixes, publish results, or promote repository stages.