Workstation install and status
praetorctl workstation is the one engine command a workstation runs to install and
inspect this engine, on every operating system (cmd/standardsctl/workstation.go,
internal/workstation/). install and status ship now; update, rollback and
schedule render are a later change (W2, W3) and are not implemented yet.
Quick start
install builds praetorctl, praetor-mcp, praetor-lsp and tribunusctl (the
Tribunus data sync) from the given checkout and places them,
with the legacy aliases of the first three (standardsctl, standards-mcp,
standards-lsp; tribunusctl has none), into a bin directory — ~/.local/bin on Linux and macOS,
%LOCALAPPDATA%\Programs\praetor on Windows by default (the per-OS default in
internal/clientsetup/roots.go, LocationBinDir). status reports what is installed
there without changing anything.
On Windows each binary is placed as <name>.exe, the only form Windows runs, and each alias
is a copy of its binary instead of a symlink, because Windows creates symlinks only with
Developer Mode or elevation (targetFileName in internal/workstation/binaries.go, #208).
The install manifest and every report keep the bare names.
Both commands take flags in any order and no positional argument. A stray positional is
refused, and so is any token after a -- terminator, since everything after -- is
positional (parseInterspersed, cmd/standardsctl/flagargs.go).
scripts/dev_install.py (make dev-install) is a thin wrapper around the same command:
it runs the MCP functional probe first, then calls workstation install --source <this
checkout> (HISS-19 — the atomic install exists in one place, not two). See
Develop against this checkout's MCP for that workflow.
Before either step the wrapper checks that the Git hooks can start their interpreter on
this host (hook_interpreter). The hooks try python3, python and py -3 in that order
and run the first that answers a version probe as Python 3.10 or newer; the wrapper resolves
the same list with the hook policy's own resolver (python_program in
.config/lefthook/scripts/toolchain.py). A name that merely resolves on PATH is not
accepted, because the python3 and python Windows puts there are Microsoft Store aliases
and no interpreters. When no candidate proves itself the install stops and names each one
with the reason it was skipped. The wrapper stores nothing in the environment: no variable
selects the interpreter. The printed report carries the result under hook_interpreter, as
the command the hooks will start and the version it stated.
See the interpreter the hooks run; the
HookInterpreterTests cases in scripts/test_dev_install.py cover each outcome.
workstation install
praetorctl workstation install --source PATH [--bin-dir PATH] [--manifest PATH] [--if-stale]
[--fleet-config PATH] [--workstation-config PATH]
| Flag | Meaning |
|---|---|
--source |
Checkout to build the four binaries from. Required; there is no default. |
--bin-dir |
Destination directory. Default: the loaded update.bin_dir setting, else the per-OS default. |
--manifest |
Install manifest path. Default: the per-user configuration directory (below). |
--fleet-config, --workstation-config |
Layered settings documents (see effective policy). Loading one requires --source to be a governed checkout (it carries the .standards.yaml the loader reads). Neither is required: a plain install --source . never touches a settings document. |
--if-stale |
Refresh an existing install only when it lags --source; otherwise install nothing and print why. See Refresh a lagging install. |
Steps, in order (internal/workstation/install.go):
- Lock. An exclusive lock directory (
.praetor-workstation-install.lockunder--bin-dir) serializes concurrent installers; a second install while one is running is refused rather than merged. - Inspect. Every installation target (each binary and its alias) is classified before anything is written. A symlink this engine did not create, or a non-regular file (a directory, device or pipe) at a target, is refused untouched.
- Back up. Anything already at a target is copied into a fresh backup directory
under
--bin-dir, skipped entirely on a genuine first install (nothing to back up). - Build and place. Each binary is built with
go build -trimpath -buildvcs=trueinto a scratch directory.praetorctl versionstill reports the exact built commit:-trimpathstrips source paths, not Go's VCS stamp, and-buildvcs=trueoverrides aGOFLAGS=-buildvcs=falsethat would leave the install unstamped and outside the engine build check. When no tracked file differs from HEAD, the build reads a fresh clone of the HEAD commit (prepareBuildSource,internal/workstation/source.go), so untracked files such as the gate receipt neither enter the binaries nor mark them-dirty; Go stampsvcs.modifiedfromgit status --porcelain, which lists untracked files. A checkout with a modified or staged tracked file is built as it stands and stamped-dirty, so a work-in-progress install carries its edits. Each binary is then staged beside its destination and swapped into place with one rename, so the replacement is atomic. Windows cannot rename a new file onto one that is memory-mapped for execution — the runningpraetorctlitself — so there the previous file is renamed aside first, freeing the name, before the new one takes it (swapInto,internal/workstation/binaries.go). Every build/placement failure restores everything this run already touched from the backup. - Manifest. The install manifest (below) is written, recording the built commit, each binary's digest, the previous install when there was one, and which settings documents (if any) were used.
An installed binary's existing file permissions are never silently widened: if a target was already chmod'd non-executable, the next install keeps it that way and refuses rather than guessing that was accidental.
workstation status
Prints one JSON object (internal/workstation/status.go):
| Field | Meaning |
|---|---|
installed, manifest |
Whether a manifest exists at --manifest, and its contents when it does. |
manifest_sha256 |
Digest of the manifest file itself, for external tooling to reference an exact reported state. |
missing_binaries |
The binaries install builds that the manifest does not record, in build order. A manifest written before tribunusctl was installed lists tribunusctl here until the next install, or a refresh, records it (missingBinaries, internal/workstation/workstation.go). Omitted when nothing is missing. |
checkout_head, up_to_date |
--source's current commit, and whether it equals the installed engine_commit with nothing under missing_binaries. Omitted when --source is not given. |
commits_behind |
How many commits --source's HEAD is ahead of the installed engine_commit: 0 at HEAD. Omitted when --source is not given, and when the installed commit is not an ancestor of HEAD or not in the checkout at all (workstation.InstallLag, internal/workstation/refresh.go). |
lock_held |
Whether an installation lock is currently held in the bin directory (--bin-dir, or the manifest's own bin_dir when not given). Status never takes the lock itself. |
clients |
Per-client configuration-root presence, resolved through C1 (internal/clientsetup/roots.go, clientsetup.Root). Every known client has a root entry; a client whose resolution fails, for example a relocation variable naming a missing directory, reports a stated reason instead of a false negative; --home selects a foreign home directory for this resolution, the same override harvest bundle --home uses. |
Refresh a lagging install
Client wrappers and agent hooks run the installed praetorctl, not the checkout's build, so
an install left at an old commit keeps serving old behavior after the checkout moves on.
workstation install --if-stale refreshes it (workstation.Refresh,
internal/workstation/refresh.go):
It rebuilds only when every condition holds, checked in this order, and otherwise installs nothing and prints the first reason that failed:
--sourcedeclares the running engine's own module in itsgo.mod. Any other checkout is skipped before the manifest or a settings document is read.- An install manifest exists (
--manifest, else the default path). A refresh never makes a first install. - The checkout is on the update branch:
update.branchfrom the selected operator settings,mainby default (effective policy). - The installed
engine_commitis a strict ancestor of the checkout HEAD, so a refresh never downgrades or moves sideways. An install at the HEAD itself qualifies only while its manifest lacks a binary (missing_binariesabove); the refresh then reinstalls the same commit and itsreasonnames the missing binaries. - No tracked file is modified. Untracked files do not block a refresh, and they stay out of the build: the refresh builds a clean clone of HEAD (install step 4), so the refreshed install passes the engine build check.
A refresh installs into the manifest's bin_dir unless --bin-dir names another, and
records the same settings documents again unless --fleet-config or --workstation-config
names another; the environment variables are not consulted, so a refresh reinstalls what was
installed. It prints one JSON object: refreshed, reason, commits_behind and, after a
rebuild, install (the same report install prints).
This repository's post-merge hook runs it after every merge
(refresh_install in .config/lefthook/scripts/hooks.py, see Git hooks): a
skip prints nothing, a rebuild prints one line. A native scheduler can run the same command.
When a refresh is refused while the install is behind the checkout HEAD—such as when
tracked files in the checkout are modified—refresh_install prints a one-line warning
naming how many commits the install lags, the checkout HEAD, and the refusal reason:
engine install: NOT refreshed, N commits behind <checkout HEAD short>: <reason>; next: clean checkout, run make dev-install.
Skips where the install is not behind stay silent.
Engine build check
praetorctl compile-context and the MCP standards_compile_context write refuse to write
when the running engine does not match the engine checkout they write into
(workstation.CheckBuildCurrent, internal/workstation/freshness.go). Without it, a client
wrapper that runs praetorctl compile-context at session start with a lagging install
rewrote the AGENTS.md register block and every vendor file with that install's older text.
The check applies only when the target directory's go.mod declares the binary's own main
module and the binary carries Go's VCS stamp. The check reads the stamp through
buildid.Stamp in internal/buildid/buildid.go, the reader praetorctl version,
standards-mcp -version and standards-lsp -version take their revision from. Every other
repository, and a go run build, which compiles the checkout on the spot, is not judged. A
stamped build matches when:
- it is a clean build and no non-test
.gofile,go.modorgo.sumdiffers between its revision and the working tree, committed or untracked; or - it was built from a modified tree (
-dirty), its executable lies inside the checkout (for examplebin/praetorctl), and every changed input is older than the executable; or - it was built from a modified tree outside the checkout, its executable lies in another
checkout of the module (the nearest directory above it whose
go.moddeclares the module), and no Go build input of that checkout differs from the build's revision. Only files outside the Go build marked it-dirty, so it is judged as a clean build of its revision.
Go marks a build -dirty for an untracked file alone, such as the gate receipt
.standards-receipt.json. The third rule covers ci generated render, which runs
compile-context in a temporary worktree of HEAD below the checkout
(internal/generated/render.go): the checkout's own bin/praetorctl renders there after a
gate run (#760). The same build fails there once the checkout changes a Go build input, because
the worktree renders HEAD without that change. An installed copy lies in no checkout, so both
builders that feed this check keep a build clean or inside a checkout when nothing tracked
changed: workstation install builds a clean clone of HEAD (install step 4), and
scripts/dev_mcp.py builds under the checkout's git-ignored bin/ (build_directory).
A work-in-progress install, built from modified tracked files into the bin directory, is
refused.
Anything else fails with one line naming the build as praetorctl version names a stamped
build (buildid.FromStamp), the reason, and the command that writes with the checkout's own
compiler:
compile-context wrote nothing: engine build does not match this checkout: build 0123456789ab lacks 3 changed Go build inputs (first internal/compiler/render.go); rebuild bin/praetorctl from the checkout or run go run ./cmd/standardsctl compile-context
compile-context --verify and verify_only never write and are never refused. Tests:
internal/workstation/freshness_test.go, internal/workstation/source_test.go (a real
refresh from a checkout holding an untracked receipt passes the check),
cmd/standardsctl/compile_context_engine_test.go (RenderWorktree tests: the render worktree
of a checkout holding the receipt),
cmd/standards-mcp/compile_context_engine_test.go, BuildDirectoryTests in
scripts/test_dev_mcp.py.
The install manifest
internal/config/install_manifest.go (S1) defines and validates the schema;
internal/workstation is the only writer (config.WriteInstallManifest), staged and
renamed into place so a concurrent reader never observes a partial file. Default location:
os.UserConfigDir()/praetor/install.json (~/.config/praetor/install.json on Linux,
honoring XDG_CONFIG_HOME; %APPDATA%\praetor\install.json on Windows) — the same
per-user configuration directory as the receipt signing key
(internal/lockdown/keys.go). It is never committed and is git-ignored.
binaries maps each installed binary to its SHA-256 digest. The reader accepts any
non-empty subset of config.InstalledBinaryNames (praetorctl, praetor-mcp,
praetor-lsp, tribunusctl), so a manifest written before tribunusctl was installed
stays readable and status reports what it lacks. An engine built before tribunusctl
joined that list refuses a manifest that records it; run hook, clients and
workstation from an engine at or after that change, or reinstall from the older checkout
(TestInstalledBinaryNames, internal/config/install_manifest_test.go).
hook, clients * and workstation * resolve settings documents in order: an explicit
flag, then PRAETOR_FLEET_CONFIG / PRAETOR_WORKSTATION_CONFIG, then the paths this
manifest recorded (rejected if the file's digest no longer matches — config.SelectOperatorSettings).
audit and gate never read this manifest; they take explicit flags only.
Not yet implemented
workstation update, workstation rollback and workstation schedule render land in
later changes. Until then, refresh an install with workstation install --if-stale
(above) or by running workstation install again with the
same --bin-dir: the manifest records the prior commit and a backup for the next command
to build a rollback on top of.