Local Development
JobCtrl is a pnpm TypeScript workspace plus a uv-managed Python worker (a Temporal worker process that executes workflows). This page is the local loop end to end: install dependencies, run the full stack, verify a change, and the frontend, docs-site, and documentation-screenshot workflows.
Read this if you are running or changing a source checkout. These commands install contributor dependencies and are deliberately different from the installed product contract. Installed users run jobctrl start and plain jobctrl <command> from any directory; they do not use this page's Git, pnpm, Corepack, uv, or checkout-relative commands.
Install
scripts/installscripts/install is the first-run path for new contributors. It checks for Node.js 22.13 or newer, Corepack, uv (the Python worker needs Python 3.11 or newer), and the Temporal CLI, offers Homebrew installs for missing machine-level tools when available, then runs the repository dependency setup: frozen corepack pnpm install, uv sync, and Playwright Chromium installs for both the web package and the Python worker. This direct entry point can install a missing standalone Corepack package; once Corepack is available, corepack pnpm install:interactive invokes the same script. It then runs jobctrl setup to detect Claude/Codex/Antigravity auth, persist enabled analysis legs, and report whether employer analysis is ready. It does not run jobctrl doctor by default because web-first users do not need the CLI profile created by jobctrl init.
scripts/install accepts --yes, --dry-run, --skip-browsers, --skip-system, --skip-doctor, and --run-doctor for non-interactive, partial, or CLI-diagnostic runs.
scripts/get is the transport-only bootstrap boundary for the bundled distribution; it neither clones a checkout nor provisions contributor tools. The public installer resolves the authenticated stable release pointer and delegates installation to the signed native installer. Contributors continue to use scripts/install and the source commands on this page. docs/public/install.sh must stay byte-for-byte identical to scripts/get; corepack pnpm docs:build checks that before building the site.
For machines that already have the system tools and browsers installed, use the non-interactive dependency sync:
corepack pnpm dev:setupcorepack pnpm dev:setup installs the Node workspace dependencies and runs uv --project workers/automation sync --extra dev, which installs the Python worker, the pinned jobstreaming==0.0.5 provider, its locked transitive dependencies, and the Python dev tools used by local checks. It does not install Temporal or Playwright browser binaries. Do not set UV_EXCLUDE_NEWER (or a global uv exclude-newer config): it makes uv treat uv.lock as needing re-resolution, so every --locked command in this repository fails or, worse, rewrites the lockfile. System Chrome/Chromium is optional; contributor testing of apply behavior must explicitly enable a browser capability first. The full worker suite also exercises guarded anonymous availability in real Chromium. Install the locked worker browser revision with uv --project workers/automation run --locked --all-extras playwright install chromium before that suite; Python CI installs its browser and Linux prerequisites. This fixture uses a disposable anonymous context and synthetic employer transport.
The source installer downloads separate web/E2E and Python-worker Playwright Chromium revisions. The bundled release instead contains exactly one managed Playwright Chromium headless shell for core discovery, enrichment, and PDF rendering, with no full Chrome/Chromium application. A system browser stays optional unless an authenticated-browser or auto-apply capability is explicitly enabled.
Run the Python setup command directly when you only need to refresh vendor auth or analysis-leg configuration:
uv --project workers/automation run jobctrl setup
uv --project workers/automation run jobctrl setup --non-interactive --json --skip-dependencies --skip-browsersRun
Native credential migration
Stop the owned development stack, then run:
scripts/dev migrate-credentialsThis invokes jobctrl credentials migrate before sourcing any environment file. It selects $JOBCTRL_DIR/.env (default ~/.jobctrl/.env), the checkout's .env, then JOBCTRL_USER_ENV_PATH (default ~/JobCtrl/.env), in the same order as the development launcher. It migrates only the fixed guided secret allowlist and writes a secret-free completion marker under JOBCTRL_DIR. Inherited shell values are never persisted. Restart the stack after success; already-running services retain their old inherited values.
For a source CLI invocation, uv --project workers/automation run jobctrl credentials migrate defaults to the active owned .env, the current directory's .env, and JOBCTRL_USER_ENV_PATH (default ~/JobCtrl/.env). Use repeated --env-file arguments when an exact source set is required. See Configuration for conflict handling, preservation, scope, and the one-time marker.
QA must use uniquely owned temporary files and a synthetic native-store namespace. Never run a migration test against these default personal paths.
Start the development stack
corepack pnpm devcorepack pnpm dev is the source-development counterpart of installed jobctrl start. It first builds the browser extension, then starts the full local fleet in dependency order: Temporal dev server, TypeScript API, Vite web app, and the Python worker. Before each component starts, the launcher stops the existing tracked JobCtrl process tree for that component, so rerunning corepack pnpm dev starts from a clean owned stack. It runs in the foreground so supervised terminals keep the child processes alive; keep the terminal open and stop the stack with Ctrl-C. The launcher tracks PIDs under .dev/pids/, writes logs under .dev/logs/, and defaults to:
- API data dir:
JOBCTRL_DIR=${HOME}/.jobctrl - API bind:
JOBCTRL_API_HOST=127.0.0.1,JOBCTRL_API_PORT=8766 - Web API base URL:
VITE_JOBCTRL_API_BASE_URL=http://127.0.0.1:8766 - Web port:
5173(JOBCTRL_WEB_PORTcan override it) - Temporal persistence:
$JOBCTRL_DIR/temporal/temporal.db(JOBCTRL_TEMPORAL_DBcan override it)
jobctrl.db and the Temporal history store form one runtime identity. Keeping both under JOBCTRL_DIR lets an interrupted workflow reconnect to the same history when the source stack is restarted from another Git worktree. To run a fully isolated stack, give it a separate JOBCTRL_DIR; do not point a shared jobctrl.db at a worktree-local Temporal database.
Whenever scripts/dev run, start, or restart selects the product web component (including the default fleet), it runs corepack pnpm extension:build once before stopping or replacing any tracked process. A build failure aborts startup and leaves the existing processes and logs intact. Docs, demo, and component sets without web skip this build, as do status and stop commands. The launcher prints the absolute dist/extension path and Chrome load/reload instructions; loading or refreshing the extension in Chrome remains manual.
Runtime Overrides
Use these source-development overrides when troubleshooting a component or running isolated or multi-worktree stacks. Most contributors can keep the launcher defaults above.
| Variable | Default | What it does |
|---|---|---|
JOBCTRL_DIR | ~/.jobctrl | Local app directory for database, settings, artifacts, logs, browser worker state, and .env. |
JOBCTRL_DB_PATH | $JOBCTRL_DIR/jobctrl.db | TypeScript API database path. The Python worker ignores it and always uses $JOBCTRL_DIR/jobctrl.db, so overriding it desynchronizes the API from the worker — prefer JOBCTRL_DIR to move both. |
JOBCTRL_CONFIG_PATH | $JOBCTRL_DIR/config.json | Non-secret Settings file written by the TypeScript API and read by both API and worker (general controls, provider metadata, browser adoption, and provider-scoped model IDs). Discovery-page values remain in SQLite. |
JOBCTRL_API_HOST | 127.0.0.1 | Local API bind host. Non-loopback hosts require explicit opt-in. |
JOBCTRL_API_PORT / PORT | 8766 | Local API port. |
JOBCTRL_API_ALLOW_REMOTE_BIND | unset | Set to 1, true, or yes to allow non-loopback API binding. This can expose private local data. |
JOBCTRL_WEB_PORT | 5173 | Requested Vite development port. |
JOBCTRL_DOCS_PORT | 4174 | Requested VitePress development port. |
JOBCTRL_DEMO_WEB_PORT | 5174 | Requested Vite development port for the browser-local demo. |
JOBCTRL_DEMO_API_PORT | 8787 | Local Wrangler port for the demo consent and telemetry API. |
JOBCTRL_DEMO_STATE_DIR | .dev/demo/wrangler | Local Wrangler/D1 persistence shared by demo migrations and the demo API process. |
VITE_JOBCTRL_API_BASE_URL | proxied /v1 | Browser API origin when not using the default Vite proxy. |
JOBCTRL_TEMPORAL_DB | $JOBCTRL_DIR/temporal/temporal.db | Temporal (the workflow engine) dev-server SQLite history store. Override it only together with an isolated JOBCTRL_DIR; sharing JobCtrl projections while switching Temporal history stores breaks workflow recovery. |
TEMPORAL_ADDRESS | localhost:7233 | Temporal server address used by the worker, CLI, and workflow-starting RPC. |
TEMPORAL_NAMESPACE | default | Temporal namespace. |
JOBCTRL_API_SSE_POLL_MS | 250 | API event-stream database poll interval in milliseconds. |
VITE_DEV_API_PROXY_TARGET | http://127.0.0.1:8766 | Vite dev-server /v1 proxy target; override it for isolated or multi-worktree stacks. |
VITE_DEMO_API_PROXY_TARGET | launcher-managed | Vite dev-server /api proxy target for demo mode. The launcher sets it to the tracked local Wrangler process so consent stays same-origin. |
VITE_GOOGLE_MAPS_API_KEY | unset | Enables Google Maps address search in the Profile form. |
JOBCTRL_SKIP_BROWSER_PREFLIGHT | unset | Set to 1 to skip the worker's startup Chromium check, for a worker that runs only non-browser activities. |
PLAYWRIGHT_SKIP_BROWSER_GC | unset | Set to 1 when running playwright install from another checkout, so it keeps the browser revisions other worktrees still use in the shared Playwright cache. |
Inspect the foreground stack from another terminal:
corepack pnpm dev:status
corepack pnpm dev:logs worker
scripts/dev listcorepack pnpm dev:status combines PID liveness with the API worker heartbeat health classification. When the worker process is alive but its heartbeat is stale, the worker row reports stale so operator status matches the dashboard.
For a detached background stack in a normal shell, use the explicit daemon mode:
corepack pnpm dev:start
corepack pnpm dev:stopcorepack pnpm dev:start prints the observed API, web, and Temporal bindings after the processes launch. Use the printed web URL rather than assuming 5173, because Vite can bind a higher port when another local JobCtrl web server is already using the requested port.
Purge A Source Workspace's Job Data
This internal maintenance command is intended for disposable test workspaces and deliberate local reset work. corepack pnpm data:purge-jobs is read-only unless it receives the exact confirmation phrase. Use it first to resolve and count the target workspace:
corepack pnpm data:purge-jobs --app-dir /path/to/disposable-jobctrl-dirBefore execution, stop every JobCtrl process using that workspace and verify that no API or worker from another worktree still has jobctrl.db open (for example, lsof /path/to/disposable-jobctrl-dir/jobctrl.db). A fresh worker heartbeat for this database blocks deletion; wait at least 45 seconds after the last heartbeat once the worker is stopped. Heartbeats do not detect every API or other writer, so the process check remains required. Any database commit by another connection between inventory and the deletion lock aborts the run without deleting or moving files. Stop the writer and repeat inventory before retrying. Then run:
corepack pnpm data:purge-jobs --app-dir /path/to/disposable-jobctrl-dir \
--confirm DELETE-ALL-JOB-DATAThe command backs up first, purges the exact-v12 local Job graph, archives live generated resume/cover-letter entries and registered job logs, removes the job/Discovery execution ledger, source-quality summaries, job-stage operational attempts, and projection-rebuilding lifecycle events, compacts SQLite, and checks that profile/templates/Discovery settings/Settings fingerprints did not change. It preserves unrelated Profile, source-control, contact/outreach, maintenance-workflow history, and non-job operational attempts. It is not a Temporal-history or secure-erasure command. The owning boundary is documented in Storage.
If post-commit SQLite compaction alone fails, the command still verifies the logical purge and preservation boundary, reports that the purge committed, and prints the recovery bundle plus free-space and compaction guidance. If those post-commit invariants cannot be verified, keep JobCtrl stopped and follow the printed recovery-bundle restore guidance.
Resolve Provisional Missing-History Executions
The inventory and refusal message list workflow/run IDs for terminated + reconciled_not_found rows. They are provisional: the worker may have reached the wrong Temporal server or lost access to the authoritative history. An absent execution cannot be canceled. Restore access to the correct history and let the reconciler settle it normally whenever that history still exists.
For a deliberate reset after permanent local history loss only:
- Stop JobCtrl API, workers, launchers, and any other writer. Verify no process has this
jobctrl.dbopen. Keep the worker stopped throughout this procedure. - Verify the workspace's configured Temporal address and namespace against the intended history store. Inspect that namespace's complete workflow inventory and each exact workflow/run ID listed by the purge. The namespace must be empty and every listed execution must be absent. An unreachable server, an empty result from a different namespace, or a permissions error is not proof. Do not restore older Temporal history after this clearance.
- Open this workspace database in
sqlite3and create a separate pre-clearance backup with.backup '/absolute/private/path/jobctrl-before-clearance.db'. Keep it with the later purge recovery bundle; the purge's own backup is taken after this clearance. Set named parameters:workflow_idand:temporal_run_idto one exact inventoried pair (SQL NULL for a null run ID), and execute the guarded statement below. Confirmchanges()is exactly one; zero means the identity or state changed and requires a fresh inventory. - Close SQLite, rerun inventory, and execute the confirmed purge while all writers remain stopped. Repeat the guarded clearance only for other exact pairs independently verified in step 2. Active stages and ordinary active workflows still block the command.
UPDATE workflow_run_projections
SET error_code = 'operator_verified_history_absent',
error_message = 'Operator verified the configured Temporal namespace empty before an offline job-data purge.'
WHERE tenant_id = 'local'
AND workflow_id = :workflow_id
AND temporal_run_id IS :temporal_run_id
AND LOWER(status) = 'terminated'
AND LOWER(COALESCE(error_code, '')) = 'reconciled_not_found';
SELECT changes();This maintenance-only clearance preserves the terminal status and records why the absent execution no longer blocks the reset. It does not assert that an unreachable execution finished, and it must not be used during normal operation.
Run individual components only when troubleshooting a specific process:
temporal server start-dev --db-filename "$JOBCTRL_DIR/temporal/temporal.db"
corepack pnpm api:dev
corepack pnpm web:dev
uv --project workers/automation run jobctrl worker
uv --project workers/automation run jobctrl doctorThe Temporal dev server binds the frontend gRPC service on 127.0.0.1:7233 and the Web UI on http://127.0.0.1:8233. The launcher passes --db-filename "$JOBCTRL_TEMPORAL_DB" so workflow history persists across launcher and worktree restarts instead of disappearing when the process exits. With Temporal running, jobctrl doctor reports Temporal: reachable. The Vite web dev server proxies /v1/* to the TypeScript API by default.
Public demo browser workspace
Run the complete local public demo—synthetic browser-local workspace plus the real local consent and telemetry Worker—with:
corepack pnpm demo:devThis is the foreground form: keep the terminal open and press Ctrl-C to stop both tracked processes. For a detached demo that returns control to the shell, use the complete lifecycle:
corepack pnpm demo:start
corepack pnpm demo:status
corepack pnpm demo:stopcorepack pnpm demo:start applies pending migrations to the local D1 store, starts the Wrangler API on port 8787, starts the demo-mode Vite app on requested port 5174, prints both observed bindings, and returns. Vite proxies /api/* to Wrangler without changing the browser-facing origin, preserving the same-origin consent and secure-cookie boundary used in production. Local D1 state persists under .dev/demo/wrangler/; override the three JOBCTRL_DEMO_* variables in the table above for isolated multi-worktree sessions. Inspect process logs with corepack pnpm dev:logs demo-api or corepack pnpm dev:logs demo-web.
The local demo does not start the JobCtrl API, Temporal, Python worker, SSE, or host-OS integrations. Only the exact value demo selects this frontend composition; the lifecycle command supplies it automatically. A missing or invalid VITE_JOBCTRL_APP_MODE keeps the normal local composition, so a mistyped value cannot produce a partially mounted app.
For the target-role suggestion browser check, start corepack pnpm demo:dev, accept the local demo gate, open Discovery → Target search, and use Suggest roles. The bundled profile fixture has version 1 and explicit Management / Director preferences. Its deterministic result is labeled model_stub with a stubbed_model_evidence warning; it is fixture evidence, not a real provider run. Edit, reject, or add the proposal, then save to exercise the same version-checked form path without production profile data or a database.
Demo mode now renders the static acceptance gate before creating IndexedDB. If the same-origin local Worker is unavailable, acceptance remains fail-closed and the workspace does not initialize. The local lifecycle runs the real Worker contract, so it supports manual cookie-persistence testing without a Cloudflare preview; the dedicated Playwright lane may still stub that contract for focused frontend tests.
Each browser/storage profile has its own IndexedDB demo workspace. It is not shared across browser profiles or through a common demo environment, but tabs and anyone using the same profile can see the same data. Private/incognito contexts are isolated. Reset rotates the workspace identity, clears pending demo actions, and deletes generated demo blobs in the same transaction. If IndexedDB is unavailable or full, the page warns that it has switched to tab-local memory; those fallback changes are neither shared nor retained after the tab closes. The demo contains synthetic data, but visitors should still not enter personal data or secrets. Product reads, filtering, sorting, pagination, details, bundled previews, and safe synchronous write actions are served from that browser-local workspace. Those writes are atomic, survive reload, and remain visible across same-profile tabs. Long-running demo controls, including the job detail's run current stage action, use deterministic queued, running, and terminal scenarios whose projections survive reload and same-profile tabs. Application dry-runs, simulated mark-applied actions, and artifact opening are rehearsals only: each adds a durable receipt that confirms no external effect occurred. Other provider-backed actions remain unavailable in the MVP. The receipt history stays inspectable in the demo shell. The demo never falls back to the product API, SSE, an external origin, or a host-OS opener.
DemoSeedValue.seedVersion is the fixture-revision boundary, separate from the IndexedDB/workspace schema versions. Changing the canonical seed must bump this value. When a seed refresh changes the destructive writer contract, also advance DEMO_WORKSPACE_SCHEMA_VERSION so an older bundle refuses the snapshot before writing or clearing blobs. Only explicitly reviewed older seed versions are refreshable: on the next initialization they are atomically reseeded with a new workspace identity and reset epoch, and pending scenarios and generated blobs are removed. A newer or unknown seed version instead requires an updated demo and leaves its durable snapshot and blobs untouched. If an allowed durable refresh cannot be written, the current seed is loaded into tab-local memory with the existing storage warning. Consent state is outside this workspace lifecycle and is not reset.
Public demo edge workers
The public demo uses Cloudflare Pages for the Vite SPA, a same-origin Worker at demo.jobctrl.dev/api/* for consent and telemetry, and an hourly retention Worker. Run the edge checks with:
corepack pnpm demo-edge:check
corepack pnpm demo-edge:test
corepack pnpm demo-edge:migrate:local
corepack pnpm demo-edge:dry-run
corepack pnpm demo:buildThe production Wrangler configs bind the EU-scoped jobctrl-demo-telemetry D1 database. Generated Wrangler bindings are intentionally ignored—the checked-in environment contract contains only the four bindings used by this package.
.github/workflows/demo-site.yml always builds and verifies the demo. Same-repo pull requests publish a static Pages preview only when DEMO_PREVIEW_DEPLOY_ENABLED=true. Production runs only from main when DEMO_DEPLOY_ENABLED=true, applies D1 migrations, deploys both Workers, and then publishes Pages. The workflow uses the existing CLOUDFLARE_ACCOUNT_ID and CLOUDFLARE_API_TOKEN secrets.
Production deployment uses the workflow controls above. Verify the affected checks on the deployed revision; repository publication is already complete.
Rollback the static site from the Cloudflare Pages deployment history, then redeploy the prior Worker versions if the fault crosses the /api/* boundary. Do not roll D1 backward destructively; migrations are forward-only and the 90-day retention worker remains safe to run during a frontend rollback. After deployment, DEMO_BASE_URL=https://demo.jobctrl.dev corepack pnpm demo:smoke checks the security headers, a direct SPA deep link, the anonymous consent read, and exact denied/granted cookie boundary.
Verify
Schema v14 migrations must run with JobCtrl stopped and must write a separate candidate. For a synthetic exact-v13 source, exercise the private boundary with python -m jobctrl.infrastructure.migrations.v13_to_v14_execute --source <source.db> --candidate <candidate.db>. The command never installs the candidate. Normal installations use jobctrl update, whose native lifecycle owns quiescence, paired backup, candidate activation, readiness, and rollback. Never run a migration executor against the live path as its candidate.
Choose the touched-surface recipe and required gates through Reliability & QA. The root aggregates do not include the separate web unit, type-level, Playwright or Storybook suites. Build the Python package when packaging behavior changes.
For reproducible 100, 1,000, and 10,000-job local scale measurements through the production projection, HTTP, SSE, preview, and Python RPC paths, use the local scale benchmark.
Pull-request CI
The repository requires maintainer approval for workflow runs from all external fork contributors. Review the proposed changes before approving a run. Issue labels and Project status do not approve code execution.
CI is plain path-filtered GitHub Actions with no routing layer: each workflow under .github/workflows/ declares the paths it owns and runs whole when a pull request or a main push touches them. typescript.yml runs the API, web, Storybook, web E2E, and extension suites, including web:lint frontend boundary checks; python.yml lints and runs the full pytest suite on each supported Python version; launcher.yml runs the native launcher race suite together with the cross-runtime migration boundary (Go opens the candidate with the locked Python migration runtime, then the TypeScript API reopens it); distribution.yml audits the fail-closed release contracts when dependency locks or packaging inputs change — the surface Dependabot PRs touch; docs-site.yml builds and verifies its rendered site in Chromium, and demo-site.yml builds its site. Both deploy on main. Two workflows deliberately take no paths filter: release-check.yml (any file can leak PII) and repo-scripts.yml, whose scripts:test suite asserts contracts across inputs that sprawl the repository (docs launch copy, version parity into launcher.go and the release workflows, demo composition, dev supervisor lifecycle).
Python CI syncs workers/automation/uv.lock with --locked --all-extras; it must never resolve the floating ranges in pyproject.toml in place of the locked candidate graph. The full worker suite runs with two pytest-xdist processes and --dist=loadfile, keeping a file's fixtures in one process while independent files overlap. Each process owns its temporary JobCtrl workspace. --max-worker-restart=0 makes a crashed or timed-out worker fail the run without an automatic restart. corepack pnpm python:test and the full worker recipe use the same settings and pass workers/automation/tests explicitly so pytest loads the worker's configuration. Focused pytest commands stay serial unless the caller requests workers; do not pay process startup for a single small file. Ephemeral Temporal test environments must start and stop only through the bounded, retried lifecycle in workers/automation/tests/temporal_env.py — two hosted lanes previously hung for GitHub's full six-hour ceiling on a wedged test-server start — and every test carries a three-minute pytest-timeout bound that dumps all thread stacks and hard-exits the wedged process (timeout_method = "thread"; a signal cannot unwind a blocked worker-pool thread), with a 45-minute job ceiling behind it. CI points TMPDIR at /dev/shm when available so hosted-runner disk latency cannot amplify the SQLite migration tests; the macOS launcher workflow still proves the migration boundary on real disk. If the SDK's automatic test-server download is unavailable locally, point JOBCTRL_TEMPORAL_TEST_SERVER_PATH at an existing Temporal time-skipping test server executable and JOBCTRL_TEMPORAL_CLI_PATH at an installed Temporal CLI. These test-only overrides must be absolute executable file paths; invalid paths fail before startup. They use the SDK's supported existing-binary parameters and retain real servers, per-test lifecycles, startup/shutdown bounds and workflow execution. Unset overrides keep the SDK's normal download/cache behavior. Dependabot staggers package-manager batches weekly, groups compatible minor and patch updates, and applies a rolling cooldown before opening a candidate lockfile.
Test And Documentation Workspaces
These contributor-only variables isolate synthetic QA, screenshot generation, and CI from a real JobCtrl workspace:
| Variable | What it does |
|---|---|
JOBCTRL_E2E_APP_DIR | Optional existing temporary parent. Every ordinary, isolated or screenshot run creates and removes only its own marked child; the parent is preserved. |
JOBCTRL_E2E_DB_PATH | Contained database path derived from the current owned allocation; not an independent caller override. |
JOBCTRL_E2E_CONFIG_PATH | Contained settings path derived from the current owned allocation. |
JOBCTRL_E2E_API_PORT | E2E API port. |
JOBCTRL_E2E_WEB_PORT | E2E web port. |
JOBCTRL_E2E_STUB_DISPATCH | Routes selected dispatches through deterministic test stubs. |
JOBCTRL_DOCS_SCREENSHOTS | Opts the Playwright run into rewriting the synthetic documentation screenshots under docs/. |
VITE_JOBCTRL_SHOW_DEVTOOLS | Shows TanStack Router and Query devtools in local Vite dev builds. |
VITE_JOBCTRL_HIDE_DEVTOOLS | Compatibility override that hides TanStack devtools even when the show flag is set. |
Never point these paths at a real ~/.jobctrl workspace. The harness retains an independent allocation capability in JOBCTRL_E2E_WORKSPACE and checks its contained database/config/state paths before use or cleanup. State JSON alone cannot authorize deletion. All modes start their own servers; a busy configured port fails instead of reusing an unidentified listener. The API startup wrapper seeds the owned database before API construction, and global setup verifies that seed receipt. See isolated browser setup.
Publish a release
Use Releasing JobCtrl for version preparation, curated notes, exact-candidate checks, protected signing, publication, and independent verification of every public channel.
Build the bundled payload
The production-payload builder currently targets Apple-silicon macOS. It is a release-engineering path, not an alternative contributor setup: the build machine still needs the source toolchain above plus the pinned go1.26.4 launcher compiler, while the resulting payload is self-contained and does not. The artifact ships only the statically linked launcher and the Go standard library BSD-3-Clause attribution — never the compiler.
corepack pnpm distribution:audit
corepack pnpm distribution:buildcorepack pnpm distribution:audit checks the component inventory, redistribution and license policy, exact external archive locks, provider-pack wheel locks, source dependency baseline, and signing policy. corepack pnpm distribution:build writes an unsigned local payload and deterministic ZIP archive under dist/distribution-real/. The build compiles the API and web app, installs the core-only Python closure, embeds the pinned Node, Temporal, Python Playwright, Playwright MCP, and Chromium runtimes, then emits the manifest, SBOM, attributions, provenance, and component-size evidence. It fails if a development tool, provider runtime, source path, unowned file, unresolved license, or unpinned external input enters the payload.
The local artifact is deliberately marked unsigned-local and cannot be promoted as a stable release. It includes both native binaries at payload/launcher/jobctrl and payload/launcher/jobctrl-installer, compiled with the locked official Go toolchain. Its local descriptor, detached unsigned-local envelope, and curl fixture contract bind the ZIP's build ID, manifest SHA-256, size, archive SHA-256, an explicit minimum-safe sequence (0 locally), and an explicit empty revocation list; those fixtures are file-only and cannot select a network channel. Signed stable/prerelease descriptors use the same canonical fields with a positive floor and sorted, unique revocation tombstones. The launcher's private runtime manifest starts the fixed loopback Temporal (7233/8233), worker, and API (8766) fleet without Vite, and records each canonical JOBCTRL_DIR under ~/Library/Application Support/JobCtrl/instances/<sha256> (override with JOBCTRL_RUNTIME_HOME). This is an artifact smoke/development surface only; scripts/dev remains the source-contributor launcher until the installer and signing phases land. Use the much smaller contract fixture while changing the builder itself:
corepack pnpm distribution:build:fixture
corepack pnpm distribution:provider-lock:checkThe extracted-payload smoke runs the real API, JSON-RPC, Temporal workflow, worker activities and SQLite persistence without provider credentials. Its private worker bootstrap supplies explicit page-interpretation and description quality model choices through LlmPort; normal spend preflight, source/citation validation and determination persistence still execute. The bootstrap is passed as test code outside the immutable payload and is never installed or selected by product configuration. The resulting semanticModel evidence identifies the synthetic boundary. This proves capture plumbing and restart preservation, not model accuracy or live-provider readiness.
Provider SDKs and their proprietary companion runtimes are not copied into the core archive. Their complete transitive wheel closures are generated from the Python lock and acquired later as isolated, hash-verified official-channel packs. Ordinary source uv sync and uv run retain the existing provider ensemble through the default provider-runtime dependency group; the payload builder explicitly selects --no-default-groups --no-dev.
First-Run TTFV Measurement
Real-path first-run time-to-value measurement is owner-run only because it uses real vendor auth, real discovery from the owner's target search settings, real job output, and real LLM spend. The wrapper lives at scripts/ttfv-real.mjs and is exposed through:
corepack pnpm ttfv:real
corepack pnpm ttfv:probe
corepack pnpm ttfv:summary -- "$HOME/.jobctrl/measurements/ttfv-real-run-"*.jsonUse node scripts/ttfv-real.mjs run ... directly on a clean checkout before dependencies are installed; the wrapper records T0 immediately before it starts scripts/install, captures a pre-work /v1/jobs baseline, across all job visibility states, and starts the real path with jobctrl run discover score tailor --limit 1 --workers 1. The summary gate accepts only full clean-run records with all-state baseline absence, discoveredAt >= T0, hashed real discovery-source proof, plus same-job API/UI/PDF proof; probe-only, seeded, and timing-only records are rejected. See developer/first-run-ttfv.md for the clean-run protocol, stop conditions, record privacy rules, and three-run summary command.
Frontend
The React frontend under apps/web follows the architecture documented in docs/architecture/frontend/: TanStack Router / Query / Form on top of the shadcn Rhea preset, Base UI behavior primitives, and Tailwind CSS v4, with a shared filterable data grid for tables and an SSE-fed invalidation router for realtime cache fan-out. Shared wrappers under apps/web/src/shared/ui/ own primitive behavior and appearance; route code must not import Radix directly or replace an accessible wrapper with a raw native lookalike.
Run the dev server:
corepack pnpm web:devCheck frontend boundaries, typecheck, and build:
corepack pnpm web:lint
corepack pnpm web:check
corepack pnpm web:buildweb:lint checks production imports and browser-capability access using the TypeScript syntax tree and capability provenance, including real generated loader controls independent of compiler diagnostics. Run corepack pnpm web:lint:test for the complete positive/negative boundary suite. Existing boundary debt has scoped exceptions; new imports cannot silently widen them. See the frontend boundary checks for the enforced rules, legitimate infrastructure access, and review limits.
Required-bullet coaching verification
Required-bullet semantic findings live in workers/automation/src/jobctrl/domain/profile/required_bullet_coaching.py and must originate in LlmPort.chat_json. Shared TypeScript fixtures cover mechanical source binding and safe acceptance using explicit model test doubles. They never encode phrase-trigger expectations. Test an identical saved claim with different model findings, provider failure without fallback, forged sources, malformed output, spend/lane binding and saves during a pending call. Pure tests and stubbed browser runs do not prove a real configured provider response; verify that separately using only synthetic source facts in an owned QA workspace.
Run these focused checks with prepared workspace dependencies:
UV_EXCLUDE_NEWER=false uv --project workers/automation run --locked --all-extras pytest -q workers/automation/tests/test_required_bullet_coaching.py
corepack pnpm --filter @jobctrl/domain-types check
corepack pnpm --filter @jobctrl/domain-types test
corepack pnpm --filter @jobctrl/contracts check
corepack pnpm --filter @jobctrl/api-client check
corepack pnpm api:check
corepack pnpm --filter @jobctrl/api exec vitest run \
test/required-bullet-suggestions.test.ts test/server.test.ts
corepack pnpm web:lint
corepack pnpm web:check
corepack pnpm --filter @jobctrl/web exec vitest run \
src/demo/capabilities.test.ts \
src/demo/DemoApiClientAdapter.test.ts \
src/demo/DemoLocalCommandExecutor.test.ts \
src/demo/DemoScenarioEngine.test.ts \
src/contexts/profile/components/RequiredBulletSuggestions.test.tsx \
src/contexts/profile/forms/profile-form.test.tsx \
src/contexts/profile/hooks/useUpdateProfileMutation.test.ts
JOBCTRL_E2E_ISOLATED=1 corepack pnpm --filter @jobctrl/web exec playwright test \
--config=e2e/playwright.config.ts e2e/tests/required-bullet-coaching.spec.ts
corepack pnpm web:build
corepack pnpm demo:build
corepack pnpm docs:build
git diff --check origin/main...HEADThe Playwright configuration creates an owned disposable synthetic workspace and starts its own API/web servers; choose available isolated JOBCTRL_E2E_API_PORT and JOBCTRL_E2E_WEB_PORT values when the defaults are occupied. Generation, rejection, acceptance, persistence/reload, delayed responses, conflicts and lost-response recovery must execute. Skips or failed browser startup do not pass this gate. The auditability checks define the preservation invariants; the frontend inventory describes other shared helpers and remaining adapter/simulation duplication.
Frontend test pyramid
Run the test pyramid (Vitest unit / hook / component, type-level tests, and Playwright end-to-end) through the root aliases:
corepack pnpm web:test
corepack pnpm web:test:watch
corepack pnpm web:test:coverage
corepack pnpm web:test-d
corepack pnpm web:e2e
corepack pnpm web:e2e:headed
corepack pnpm web:e2e:live-worker:test
node scripts/live-worker-browser-smoke.mjsweb:e2e builds a fresh production web bundle inside its owned temporary workspace and serves it with Vite preview. Browser contexts load bundled chunks instead of repeatedly loading the development module graph. The same-origin /v1 and SSE proxy still reaches that run's real API. One Playwright worker preserves the shared seeded database's mutation order; every test still gets a fresh browser context. Build failures fail the run before preview can start.
For a small focused browser run, the development server avoids the fresh build cost:
JOBCTRL_E2E_WEB_SERVER=dev JOBCTRL_E2E_ISOLATED=1 corepack pnpm --filter @jobctrl/web exec playwright test --config=e2e/playwright.config.ts e2e/tests/dashboard.spec.tsOnly preview (the default) and dev are accepted. Both modes allocate the same owned synthetic workspace and retain the same assertions. The focused browser recipes in scripts/checks.toml and the focused real-RPC CI step explicitly use dev; the full CI browser suite uses the default production preview. The separate demo workspace suite keeps its development server for source-module browser probes.
web:e2e uses the deterministic stub dispatcher and seeded worker heartbeat. Its cover-letter PDF measurement cases also require the Poppler system tools pdfinfo, pdffonts, pdftotext and pdftoppm on PATH. Install them during environment preparation, before browser tests: on Ubuntu/Linux CI use sudo apt-get update && sudo apt-get install --yes poppler-utils; on macOS use brew install poppler. Verify preparation with corepack pnpm --filter @jobctrl/web e2e:check-pdf-tools. The materials test preflight fails explicitly if any required tool is unavailable; it never skips measurements or installs system packages during a test run.
The direct node scripts/live-worker-browser-smoke.mjs command is a separate, slower, opt-in integration lane: it starts an owned Temporal dev server, the standard Python worker registry, the real API JSON-RPC dispatcher, and Vite against one disposable synthetic workspace. Its single Cover workflow uses a loopback-only deterministic model boundary and proves queued dispatch, real worker lifecycle/projection events, API/SSE identity correlation, and terminal browser state without provider spend or an application submission. A capability-validated worker bootstrap ignores dotenv, native credential stores, and persisted provider connections, asserts isolated credential homes, and enables only the authenticated loopback provider route. The direct Node launcher uses the same credential list before its first child process, so Corepack, pnpm, and Playwright receive only the scrubbed environment. There is deliberately no package-manager alias for the live run because its outer Corepack and pnpm processes would start before the launcher; invoke the direct Node command from the repository root. Persisted process-group capabilities let the outer teardown verify cleanup after a runtime supervisor failure, while an explicit persistent guard prevents the allocator's exit cleanup from erasing an unverified workspace. Run web:e2e:live-worker:test first when changing these bootstrap, lifecycle, or cleanup guards. Logs and browser evidence remain under dist/live-worker-smoke/; temporary app and Temporal state are deleted only after every owned process group is verified stopped.
The package-local commands are equivalent and useful when working directly inside the web package:
corepack pnpm --filter @jobctrl/web test
corepack pnpm --filter @jobctrl/web test:watch
corepack pnpm --filter @jobctrl/web test:coverage
corepack pnpm --filter @jobctrl/web test-d
corepack pnpm --filter @jobctrl/web e2e
corepack pnpm --filter @jobctrl/web e2e:headedRun Storybook locally and against the built assets:
corepack pnpm web:storybook
corepack pnpm web:storybook:build
corepack pnpm web:storybook:testweb:storybook:test runs the Storybook test runner over the static build, which executes the per-story play() interactions and the @storybook/addon-a11y axe checks (critical+serious violations fail).
Browser Extension
The Manifest V3 browser extension lives under apps/extension. It is the local capture/autofill client and optional integrated-Discovery browser transport for the TypeScript API, not a hosted/browser-store package.
corepack pnpm extension:check
corepack pnpm extension:test
corepack pnpm extension:build
corepack pnpm extension:e2eThe extension E2E suite requires headed Chromium. On Linux without a display, run xvfb-run --auto-servernum corepack pnpm extension:e2e; the CI browser installation uses Playwright's --with-deps option to install Xvfb. A missing browser or display fails the required tests.
corepack pnpm extension:build writes the unpacked extension bundle to dist/extension/. The source launcher also runs this build before starting or restarting the product web component. Open chrome://extensions, enable Developer mode, and choose Load unpacked with that directory for manual QA, or click Reload on its existing unpacked-extension card after rebuilding. Reload any application tabs that were already open so Chrome injects the newly built content script into them. Chrome can otherwise load the rebuilt popup from disk while retaining the old background service worker. The popup validates their shared message protocol, reports Extension update incomplete, and disables pairing/actions instead of rendering missing fields or claiming readiness. Reload the unpacked extension card to make both halves use the same build. The content script matches http://*/* and https://*/*, which Chrome presents as access to all ordinary web sites; browser-internal and extension pages remain outside that match. Autofill stays passive until an explicit extension action; the background service worker also polls for bounded Discovery tasks and executes HTTP/API work in the service worker and rendered-page work in temporary tabs in the profile where the extension is loaded. LinkedIn jobs use an active tab moved into an unfocused window; other pages use inactive tabs. Saving the token in that popup explicitly selects its extension-local installation UUID for Discovery; merely retaining an older token does not win a race with another Chrome profile. The extension uses activeTab, alarms, declarativeNetRequest, scripting, and storage; HTTP(S) page and network access are wildcarded because the broker can lease arbitrary configured public job sources. Capture/autofill API traffic remains loopback-only, and remote service-worker requests exist only for active Discovery leases. corepack pnpm extension:e2e builds the bundle and proves wildcard page reach, the generic-form review path, loopback-only capture/autofill traffic, and a synthetic Discovery API lease completed from a non-HTML source origin with a cookie set in that same persistent Chrome context. It also proves a hanging request hard-times out without leaving a tab and that a public source redirect cannot reach a loopback target. After every rebuild, reload the installed extension before using its selected-installation heartbeat as product-path evidence. A current popup with an already stored token reports whether this exact installation is selected and offers Use this Chrome profile for Discovery, so recovery does not require copying the token again.
Discovery and Enrich also run without loading the extension. Each acquisition setup prefers the selected installation only when its bounded status probe reports connected; otherwise it uses the existing public HTTP or anonymous Playwright path. Site, DNS, access, and cancellation failures do not switch transport. Integrated fallback never enables copied-profile access.
The focused e2e/tests/optional-extension.spec.ts browser check uses the owned synthetic workspace and real API/UI. Its dispatcher acknowledges launches without running a worker or contacting job sites. Persisted production worker fixtures in test_optional_extension.py and test_enrichment_politeness_gate.py cover acquisition in both modes separately.
Docs Site
The documentation under docs/ (minus internal planning docs) is also a static VitePress site, configured in docs/.vitepress/config.ts. The site publishes the user guide, developer guide, architecture docs, and reference docs behind a hero landing page (docs/index.md); docs/plans/, docs/incidents/, and the repo-facing docs/README.md map stay repository-only, and links that point at unpublished or repo-root files are rewritten to GitHub URLs at build time.
corepack pnpm docs:dev
corepack pnpm docs:build
corepack pnpm docs:check:runtime
corepack pnpm docs:previewcorepack pnpm docs:dev is the foreground server and stops with Ctrl-C. For a tracked background server that returns control to the shell, use:
corepack pnpm docs:start
corepack pnpm docs:status
corepack pnpm docs:stopcorepack pnpm docs:start prints the observed VitePress URL (requested port 4174) and returns. Use that URL because VitePress may select a higher port when the requested one is occupied. Its PID and log use the shared .dev/ launcher state; inspect the log with corepack pnpm dev:logs docs.
corepack pnpm docs:build fails on dead internal links, then runs scripts/check-docs-site-links.mjs, which fails if any href/src emitted into the built site does not resolve to a built page or asset (this catches links to pages relocated by rewrites, which VitePress's source-level dead-link check cannot see). Together they are the docs link-integrity gate. The docs workflow (.github/workflows/docs-site.yml) runs these checks and the rendered Chromium regression gate on matching pull requests and pushes to main, before uploading the site artifact. It also supports manual runs. Mermaid diagrams render client-side in the browser, so a build that passes can still contain a diagram that fails to parse. corepack pnpm docs:check:runtime starts a fresh preview and checks hydration, images, navigation, responsive diagrams, search, the comparison screenshot-carousel interaction, and the desktop/mobile comparison layout (including keyboard access to its wide table) in Chromium. It also intercepts the Google tag and proves the documentation cookie banner makes no analytics request before acceptance, persists both choices, emits SPA page views only after acceptance, and stops tracking plus clears site analytics cookies after withdrawal. Choose whether this browser gate is required using Documentation verification, and run it after corepack pnpm docs:build when required. Note that corepack pnpm docs:preview snapshots the built file list at startup: after any rebuild, restart the preview server or hashed assets will 404. Deploys to Cloudflare Pages run from main once the DOCS_DEPLOY_ENABLED repository variable and the Cloudflare credentials are configured.
The docs theme sends its own sanitized page_view for the initial accepted route and each VitePress navigation. In the GA4 web data stream for G-KB495KG6MS, keep Enhanced measurement → Page views → Page changes based on browser history events disabled. The tag's send_page_view: false setting suppresses only its automatic load-time view; leaving GA4's separate history listener enabled would double-count client-side navigations. The repository runtime check stubs Google and proves the manual client contract; the matching data-stream setting and live event count are owner-side deployment checks.
Documentation Screenshots
Public screenshots are generated from synthetic data only — never from a real ~/.jobctrl workspace.
corepack pnpm docs:screenshotsThe command runs apps/web/e2e/tests/docs-screenshots.spec.ts through the Playwright e2e harness: it seeds a disposable E2E app directory with the local QA seed (apps/api/test/qa-seed.ts — fake candidate, jobs, stage state, scores, materials, requirement-fit evidence, employer analysis, artifacts, pipeline-operations lineage/projections/ETA samples, and a current worker heartbeat), starts the API and web app on E2E ports, and writes PNGs to docs/assets/screenshots/. The manifest covers all production primary routes, detail workspaces, profile-import steps, Settings routes, and fixed mobile companions. The internal capture manifest lives in apps/web/e2e/tests/docs-screenshots.spec.ts; it is not part of the public Product Tour. No real LLM provider, job source, Gmail account, or browser submission is involved. After a successful gallery capture, scripts/render-docs-brand-assets.mjs renders the docs favicon/header marks, app icons and social preview from the canonical apps/web/public/favicon.svg and the freshly captured synthetic dashboard. The header has explicit light and dark assets so it follows the reader's selected docs theme independently of the operating-system theme.
The spec is opt-in: it only writes when JOBCTRL_DOCS_SCREENSHOTS=1 is set, which corepack pnpm docs:screenshots does for you. A bare full e2e run (corepack pnpm --filter @jobctrl/web e2e) skips it, so QA runs never rewrite the committed screenshots.
The wrapper allocates isolated ports and a marker-owned temporary workspace by default. To pin them while running multiple worktrees, create a disposable temporary parent and override the parent and ports:
mkdir -p /tmp/jobctrl-docs-shots
JOBCTRL_E2E_APP_DIR=/tmp/jobctrl-docs-shots \
JOBCTRL_E2E_API_PORT=8890 \
JOBCTRL_E2E_WEB_PORT=5290 \
corepack pnpm docs:screenshotsRefresh checklist: run the command on a clean checkout, confirm every asset in the internal capture manifest was rewritten, review every desktop and mobile PNG for private data, broken layout, clipped content, and local-path leaks, and inspect Pipelines for the seeded execution, three source families, two reconciliation steps, available worker capacity, visual stage flow, stop/recovery controls, and active work. Confirm Jobs shows one table with a static Active/Deleted/Hidden Job state column filter defaulted to Active, and that the default view keeps source, compensation detail, warnings, resume template, and discovery date available through Columns rather than widening the initial table. Check Job Detail's labeled metadata, six assessment cells, visible requirement evidence, and readable artifact labels/actions. Check Apply Review's left queue, sequential review content, and Submit gates table with Gate/State/Detail columns. Inspect Profile/Settings for strong input outlines and section rules, then check Artifact Detail's preview after its audit details, and the mobile Profile/Evidence/record-card reflows without horizontal overflow. Keep raw IDs and paths inside technical disclosures. Open the rendered Product Tour and confirm it contains only user-facing product explanations and screenshots—not the asset manifest, capture commands, or viewport QA criteria. Confirm the homepage hero copy at docs/public/assets/screenshots/dashboard.png is byte-for-byte identical to the gallery dashboard screenshot, update the tour if screenshot names changed, and finish with git diff --check.
Safety rules: never point generation at ~/.jobctrl or a shared temporary root; an override must name an existing disposable child directory that the runner can use only as a parent for its unique marker-owned workspace. Never use real resumes, databases, logs, Gmail tokens, or browser profiles; do not run apply automation, mailbox scans, real crawling, or real LLM calls for screenshots; keep output deterministic (fixed per-surface viewports, synthetic database, seeded pipeline operations and heartbeat, no external providers). The Profile preview renderer runs with the Python lock held; its fixed dependency cutoff is defined in workers/automation/pyproject.toml, not inherited from a caller-provided UV_EXCLUDE_NEWER value. A stale lock must fail rather than being rewritten by screenshot generation.
Launch Demo Asset Inventory
The launch demo assets each prove one product invariant from synthetic data. Each asset is classified A (static, already covered), B (static, new seed state), or C (dynamic/lifecycle — a driven flow that is defined here, not executed by CI), and maps to a Current row in the repository-only claims ledger (docs/claims-ledger.md), which is the committed source of truth for every claim below.
| # | Asset | Class | Claim(s) | Regeneration / proof | Status |
|---|---|---|---|---|---|
| 1 | First run → empty dashboard | B | CL-060, CL-072 | Empty-workspace seed variant + a docs-screenshots.spec.ts onboarding surface | Deferred — needs an empty-workspace seed variant + capture surface |
| 2 | Resume / profile import | B/C | CL-080 | corepack pnpm docs:screenshots → profile-import-upload.png, profile-import-preview.png, profile-import-confirm.png; actual import remains a separately driven lifecycle | Static wizard Covered; import mutation lifecycle Defined (class C) |
| 3 | Discovery → scored jobs + requirement fit + provenance | A | CL-001, CL-010, CL-011, CL-020 | corepack pnpm docs:screenshots → jobs.png, job-detail.png (seed: scored job, job_requirement_fit_items, job_bullet_provenance) | Covered |
| 4 | Apply-review audit surfaces | A | CL-023, CL-024, CL-030 | corepack pnpm docs:screenshots → apply-review.png (seed: approved generation, evidence, change_annotations) | Covered |
| 5 | Failed refresh preserves last accepted artifact | B | CL-025 | Regression tests apps/api/test/resume-templates.test.ts ("keeps the last accepted resume artifact when the PDF render fails"; "reports refresh unavailable without hiding the last accepted artifact") and apps/api/test/resume-review-drafts.test.ts ("fails the render and preserves prior approved artifacts …"); run corepack pnpm api:test | Covered — invariant proven from fixture |
| 6 | Tailoring gate rejects an unsupported claim | B | CL-021 | Grounding-gate regression workers/automation/tests/test_claim_grounding.py (a claim whose text is absent from the shipped resume is flagged ungrounded with an inspectable reason — the CL-021 fail-closed behaviour) and workers/automation/tests/test_coverage_audit.py (fabricated/stuffed keywords fall into missing); the apply-review rendering of the resulting blocker is seeded in apps/api/test/qa-seed.ts and asserted by apps/api/test/application-feedback.test.ts | Covered — gate + surface proven from fixtures |
| 7 | Dry-run apply completes + live-approval gate + blocked-channel evidence | B / C | CL-030–CL-034 | Approval card + dry-run run (qa-run-1) via corepack pnpm docs:screenshots; live blocked-channel evidence via a driven dry-run (capability shipped: approval binding + dry-run evidence) | Approval card + dry-run run Covered; live blocked-channel evidence Defined (class C) |
| 8 | Spend-ceiling stop + health surface | B / C | CL-040, CL-041 | Health surface with an llm_spend-at/over-budget seed fixture + capture; stop lifecycle via a driven run (spend ceiling shipped) | Deferred — needs an llm_spend seed fixture + health capture; stop lifecycle Defined (class C) |
| 9 | Reliability demo — kill worker, restart, resume | C | CL-050 (TR-008) | scripts/reliability-demo.sh drives DurabilityProbeWorkflow (a hermetic durable-timer probe — no crawl/LLM) on an isolated stack; kills the worker by captured PID tree and asserts the same run ids resume in Temporal + the read-model projection. Probe covered by workers/automation/tests/test_workflow_durability_probe.py; see Reliability & QA → Durable-Execution Recovery Demo | Defined — self-asserting, re-runnable script (verified locally) |
Deferred static assets (1 and 8 health capture) are launch-set follow-ups: they need a new synthetic seed variant or capture surface, not a missing product capability. Class-C assets (2 actual import mutation, 7 live evidence, 8 stop lifecycle, 9) are defined driven flows, never faked with a staged static image. No asset regenerates from a real workspace.
Screening CLI
The installed CLI supports jobctrl screening read JOB_ID and jobctrl screening write JOB_ID command.json. From source use the prepared locked interpreter with PYTHONPATH=workers/automation/src workers/automation/.venv/bin/python -m jobctrl.cli screening read JOB_ID. The user reference defines command fields and source/privacy boundaries.