Skip to content

Repository Onboarding Guide

Onboard one of your existing repositories into declarative governance with Praetor.

Six commands scaffold config, pin the lock, compile context, record debt, prepare a devcontainer, and verify governance.
Repository onboarding path
Text description
  • Onboarding commands, in order: 1. Scaffolding (praetorctl init), 2. Lockfile Pinning (praetorctl profile set --lock-source-root), 3. Context Recompilation (praetorctl compile-context), 4. Brownfield Baselining (praetorctl baseline --record --allow-increase), 5. Devcontainer Setup (praetorctl devcontainer generate --source-root), 6. Audit Verification (praetorctl audit --offline).
  • Boxes: Target repository (already carries AGENTS.md), Quickstart (praetorctl adopt), Audit passes (the quickstart), Remaining gaps (does not pass audit on its own).
  • Target repository → 1. Scaffolding (start) → 2. Lockfile Pinning → 3. Context Recompilation → 4. Brownfield Baselining → 5. Devcontainer Setup → 6. Audit Verification.
  • Quickstart → 6. Audit Verification (skips 1-5) → Audit passes (audit passes).
  • 6. Audit Verification → Remaining gaps (remaining gaps).
  • Scenario 1, init: Write the configuration, then compile the vendor files from AGENTS.md. init reads AGENTS.md beside the manifest; without it, init writes no agent files. createInitialManifest and initBaselineAndLockfile write the manifest, an unpinned placeholder lockfile and a zero-debt baseline. initAgentContext runs the compile-context write: Git ignores .workingdir/evidence/, the text register is spliced into AGENTS.md, and the six vendor files and persona copies are compiled and linted.
  • Scenario 2, profile set: Pin the lockfile and write the policy catalog from the Praetor checkout. profile set pins placeholder lockfile digests from the reviewed Praetor source root. devcontainer generate and audit require sha256 digests; pinning also materializes the catalog under .config/archetypes/.
  • Scenario 3, compile-context: Recompile the vendor files and persona copies; rerun after every AGENTS.md edit. compile-context runs the same write that init ran. writeAgentSurfaces copies every .agents/agents persona into each client persona directory, then the caveman lint runs.
  • Scenario 4, baseline: Snapshot legacy technical debt into baseline file. HISS scanner sweeps the codebase for existing infractions. Initial legacy debt is recorded with --allow-increase and --reason="<why>" to permit non-zero count.
  • (Shortened to 2500 characters; docs/figures/onboarding-path.ts holds the full content.)

1. Quickstart

Prerequisites: praetorctl installed (workstation install); a Praetor Git checkout (adopt refuses a git archive export, see source roots), written /path/to/praetor below, which --lock-source-root names so the lock can pin your profile and facets to content digests; a Git repository with your files committed; a known hosting forge, meaning an origin remote on github.com or repository.forge declared in .standards.yaml (any other host gets no Paperclip harness and the audit fails on its absence; Hosting forge). Run these in the repository root. Each command exits 0 in a new Go module (a go.mod and one source file, a github.com origin remote, committed):

# 1. Preview what adoption would write; the plan needs the lock source, as the real run does
praetorctl adopt --dry-run --lock-source-root=/path/to/praetor

# 2. Adopt: manifest, pinned lock and catalog, baseline, agent files, DevContainer, hooks, CI gates
praetorctl adopt --lock-source-root=/path/to/praetor

# 3. Stage what adoption wrote; audit reads tracked files
git add -A

# 4. Verify the configured governance contract
praetorctl audit --offline

One adopt run is the whole onboarding. It detects the profile (the report's Archetype: line); when that is not the profile you want, pass --profile on the first run, or move an adopted repository with praetorctl profile set <profile> --lock-source-root=/path/to/praetor, then refresh with praetorctl adopt --lock-source-root=/path/to/praetor (add --force for the files profile set lists; the plain run also refreshes the Paperclip harness, which profile set does not check) (Changing the profile, facets or catalog). --dry-run needs --lock-source-root on a first adoption because the plan resolves the policy the lock would pin; without it the dry run exits 1 with new lock pins require an explicit verified lock source root. The files adoption writes are listed step by step in What Adoption Scaffolds Automatically. --offline makes the audit read nothing from the forge: the live Actions permission, workflow run and branch-protection checks report as not made.

HISS debt is recorded in .standards-baseline.json; README presentation debt uses .readme-baseline.json, with independent per-file counts and ratchets. To enforce README presentation, set gate.readme_presentation: true in .standards.yaml. For an older mixed baseline, praetorctl adopt moves README entries before its baseline checks; --dry-run previews the move. An existing README baseline beside legacy README entries is refused so neither snapshot is overwritten. See README migration and enforcement.

Staged onboarding with praetorctl init

praetorctl init is the staged alternative. It requires canonical AGENTS.md instructions: it compiles the agent files from it and writes none without it. It is not a step before adopt, and it refuses once .standards.yaml exists (ensureManifestAbsent in cmd/standardsctl/init.go), so do not run it after adoption.

# 1. Initialize configuration: writes .standards.yaml, baseline, and placeholder lockfile
praetorctl init --profile framework --facets security:high,api:public-contract

# 2. Pin the lockfile and write the policy catalog from the Praetor checkout
praetorctl profile set --lock-source-root=/path/to/praetor

# 3. Recompile the agent files and persona copies (rerun after every AGENTS.md edit)
praetorctl compile-context

# 4. Snapshot legacy technical debt infractions to prevent CI failure
praetorctl baseline --record --allow-increase --reason "<why>"

# 5. Prepare a portable devcontainer from reviewed Praetor sources
praetorctl devcontainer generate --source-root /path/to/praetor

# 6. Verify the configured governance contract
praetorctl audit --offline

praetorctl init writes .standards.yaml, a zero-debt .standards-baseline.json and an unpinned .standards.lock placeholder with no content digests, and prints a warning saying so. devcontainer generate and audit then fail on the missing digest; pin the lock afterwards with praetorctl profile set --lock-source-root=/path/to/praetor (which also materializes the policy catalog under .config/archetypes/).

Init does not make the audit pass on its own. The audit stops at its first failing gate, so an init-only repository meets the gaps one at a time: no branch ruleset (praetorctl sync writes it), with docs:seo-portal no documentation gate (missing tools/markdownlint), with api:public-contract in a Go module no API compatibility gate (missing tools/apicompat/gate), no HISS-11 supply-chain exception, no .paperclip/harness.json, no agent definitions and no git hooks. Adoption writes all of them, which is why the quickstart uses it; init is a staged alternative that does not pass the audit on its own.

Step 1 writes your repository's identity into .standards.yaml. repository.owner and repository.name come from the origin remote. Without a remote the owner is forge.default_owner from your operator settings, selected with --fleet-config, --workstation-config or --manifest (see framework, forge and topology), and the name stays empty. Praetor ships no default owner: a field it cannot resolve is written empty and reported as [WARN] repository.owner not detected; set it in .standards.yaml (or repository.name), and you set it by hand (initRepositoryIdentity in cmd/standardsctl/init.go; TestInit_3D_RepositoryIdentity in cmd/standardsctl/forge_owner_test.go).


Planning-only adoption through MCP

standards_adopt accepts profile and comma-separated facets, matching the CLI's --profile and --facets selection. For example:

{"path":"/workspace/project","source_root":"/workspace/praetor","profile":"planning-artifacts","facets":"agent:sandboxed","record_baseline":false,"dry_run":true}

Both paths must be allowed by the server's existing root policy. Inspect the preview, then use the same selection with dry_run: false for an authorized application. Omitted or empty selection retains the shared adoption defaults; an empty facet string does not clear them. The adapter accepts at most 64 comma-separated entries and 8192 facet bytes, with a 128-byte profile bound. The shared pinned catalog validates selected identities. Existing repository configuration retains its normal preservation/force semantics.

The planning profile prepares governance without inventing Go or Rust manifests. It does not qualify a native build, boot, release, running DevContainer or agent activation. Those stages need their own selected checks and execution evidence.

2. Onboarding Workflow Stages

Step Action Command Expected Output
1. Scaffolding Create declarative configuration and compile agent files praetorctl init .standards.yaml, an unpinned .standards.lock placeholder and a zero-debt .standards-baseline.json created. The agent files are then written the way step 3 writes them (initAgentContext in cmd/standardsctl/init.go, adopt.CompileAgentContext): in a Git work tree the Praetor private-artifact block is merged into .gitignore unless Git already ignores the configured evidence directory (register.evidence.dir, default .workingdir/evidence/); the text register is spliced into AGENTS.md; the six vendor files CLAUDE.md, .cursor/rules/hiss-invariants.mdc, .github/copilot-instructions.md, .windsurfrules, .gemini/GEMINI.md and .codex/rules.md and the persona copies are compiled. An AGENTS.md the caveman lint rejects fails init after the files are written; fix it and run step 3 (cmd/standardsctl/init_evidence_test.go).
2. Lockfile Pinning Pin the lockfile and write the policy catalog from the Praetor checkout praetorctl profile set --lock-source-root=/path/to/praetor .standards.lock pinned with sha256 digests; policy catalog written under .config/archetypes/. Devcontainer generation and audit require this pin.
3. Context Recompilation Recompile vendor files and persona copies praetorctl compile-context The vendor files rewritten from AGENTS.md (all six unless agent_clients in .standards.yaml selects fewer); each persona in .agents/agents copied to .claude/agents, .github/agents, .gemini/agents and .codex/agents. praetorctl compile-context --verify checks the same files and writes nothing.
4. Brownfield Baselining Snapshot legacy debt praetorctl baseline --record --allow-increase --reason "<why>" .standards-baseline.json populated with existing debt.
5. Devcontainer Setup Prepare a portable bootstrap praetorctl devcontainer generate --source-root /path/to/praetor JSON and exact source companions prepared; build and startup remain separate checks.
6. Audit Verification Verify configured governance and debt-ratchet gates praetorctl audit --offline After init alone the audit fails at the first gate init leaves open (see the gaps above); once those are filled every executed gate reports pass, and skipped or unsupported coverage remains explicit.

3. Brownfield Technical Debt Ratcheting

Legacy infractions recorded in .standards-baseline.json will not fail CI status checks:

  • Monotonic Ratchet: Technical debt must decrease over time ($V_{\text{total}}(t_1) \le V_{\text{total}}(t_0)$).
  • Touched-File Clean Rule: Any legacy file modified during a pull request revokes previous exemptions and must be refactored clean. A rejection counts a touched file's findings as not in the baseline and baselined; both fail (A HISS rejection names the violations).
  • Stale Entries: A cleanup that lands without praetorctl baseline --record leaves baseline entries that match nothing. praetorctl audit warns with their count; --max-stale-baseline-entries=N fails it past N (A baseline looser than the tree is reported).
  • Moved Code: An entry is keyed by its rule, file and the function that holds the finding, not by its line, so lines inserted above a baselined function need no re-record. A renamed function or file does: praetorctl baseline --record, without --allow-increase, as the count is unchanged. A baseline recorded by an older engine keeps working and gains these keys at its next record (A baseline entry survives a line shift).
  • Initial Debt & Waivers: Because praetorctl init seeds a clean baseline (0 infractions), recording existing debt in a brownfield repository represents an initial increase and requires an explicit, justified increase: praetorctl baseline --record --allow-increase --reason "<why>". A first .gosec-baseline.json with findings and every gosec baseline increase are refused. Fix findings, justify a line-level #nosec Gxxx -- reason, or declare a dated per-file gosec exception; --allow-increase only permits HISS debt increases. The same HISS-13 exception path applies to any subsequent unavoidable architectural exception that raises the recorded count. The reason is stored in the baseline alongside the raised count; there is no CLI command or file that mints a standalone signed waiver. The hiss-waiver repository label is a scaffolded taxonomy entry with no enforcement attached.
  • After a Praetor upgrade: a newer build can add a check that finds debt in code nobody changed. The ratchet still refuses the higher count, but it no longer calls those findings introduced: when the scanned code is unchanged since the commit commit_sha names in .standards-baseline.json, or since the commit that last changed that file, the rejection tags them (check added or changed since the baseline); a baseline without commit_sha gets the neutral (not in the baseline). Either way the rejection names the remedy: fix the findings, or record them with praetorctl baseline --record --allow-increase --reason "<why>". praetorctl baseline --verify --all-violations lists every failing finding first without rewriting the file (A HISS rejection names the violations).

Flavors

A flavor names the stack of a repository within its profile and decides which templates, settings and toolchains flavor audit expects. praetorctl flavor list prints the table below and praetorctl flavor inspect <name> prints one flavor's required templates, settings and toolchains. Detection tries the flavors in this order and stops at the first match (builtinFlavorList in internal/flavor/definitions.go); rows and order are checked against that list by TestFlavorTable_MatchesTheBuiltinFlavors in internal/flavor/flavor_table_test.go.

Flavor Profile Detected by
os-image os-image packer/*.pkr.hcl, mkosi.conf, build/mkosi.conf or Kconfig fragments under kconfig/
infra-k8s container-image Chart.yaml, kustomization.yaml or helmfile.yaml at the root
python-ml app-service a machine-learning dependency in pyproject.toml or requirements.txt
native-gpu-systems native-gpu-systems meson.build (root, core/ or libvmaf/) or CMakeLists.txt
rust-library library-client Declared library-client on a Cargo crate or workspace with library targets and no published binary target; a helper package with publish = false or publish = [], its own or inherited from [workspace.package], may contain binaries. Without that declaration a Cargo repository classifies as native-gpu-systems and keeps rust-systems
rust-systems native-gpu-systems Cargo.toml
frontend-svelte app-service package.json with svelte.config.js, src/routes or vite.config.ts
typescript-node app-service package.json without svelte.config.js or src/routes
mobile-flutter app-service pubspec.yaml
jvm-service app-service pom.xml, build.gradle, build.gradle.kts, mvnw or gradlew
go-service framework go.mod with cmd/, Dockerfile or docker/
go-library framework go.mod with pkg/ or internal/
agentic-autonomous framework never detected; select it by name

When your repository matches no row, adoption still succeeds: it scaffolds no flavor templates and prints a "Not applicable" warning that names praetorctl flavor apply --flavor=<name>. flavor audit and flavor apply refuse until you either add one of the markers above or pass --flavor=<name>; gate run has no such flag, so there the remedy is the marker or a declared profile that fits.

Flavor detection does not guess

Detection reports what matched, or reports that nothing did. It no longer falls back to a plausible answer, because a repository that matched nothing was previously indistinguishable from one that is a Go library.

  • The profile decides which flavors are candidates. A flavor names the stack within a profile, so flavor.Resolve (internal/flavor/resolve.go) first takes the profile .standards.yaml declares, else the one internal/classify reads from the markers, and then picks among the flavors implementing it. flavor audit, flavor apply, praetorctl adopt and the Hindsight distiller all resolve this way, so they name the same flavor for one checkout. Where the profile has no flavors, they report not applicable (internal/flavor/resolve_test.go).
  • python-ml requires a declared machine-learning dependency. It previously fired on any pyproject.toml, so every Python repository was reported as a PyTorch pipeline and then audited against ML tooling it had no reason to install.
  • os-image is matched by packer/*.pkr.hcl, mkosi.conf, build/mkosi.conf or Kconfig fragments under kconfig/ (a kernel forge), and is tried before the language flavors. An image forge carries a go.mod for its build CLI and a pyproject.toml for its verification suite, so whichever language flavor claimed it first would describe the tooling rather than the product. The Packer marker is a glob and matches only regular files: an empty packer/, or one holding only a README, is not a forge. See archetype authoring.
  • A repository with two kinds of infrastructure as code, from Terraform (*.tf), Ansible (ansible.cfg or roles/*/tasks/main.yml), Helm (Chart.yaml or helmfile.yaml) and Kustomize (kustomization.yaml), each looked for at the root and up to three directories below it, gets the gitops-infra profile ahead of os-image. A cluster repository that keeps one Packer template for its node image is classified by what it deploys, and one kind alone, such as an image forge's Terraform test environment, decides nothing (TestByMarkers_Positive_ClusterRepositoryIsGitOps in internal/classify/gitops_test.go). gitops-infra has no flavor, so flavor detection reports not applicable for it.
  • agentic-autonomous never auto-detects and must be selected by name. Its markers — .agents/ and .paperclip/harness.json — are both written by adoption itself, so detecting on them describes the governance tool rather than the repository.
  • Where nothing matches and no flavor is pinned, one decision (flavor.IsNotApplicable) governs every caller, so adoption and the audit reach the same verdict: praetorctl adopt scaffolds no flavor templates and warns "Not applicable" (internal/adopt/flavor_report_test.go), a bad flavors pin is an adoption error, never that warning (internal/adopt/flavor_report_test.go), and flavor audit prints "Skipped, not applicable" with the same reason and exits 0 (cmd/standardsctl/flavor_skip_cli_test.go). gate run records its Flavor Conformance stage as not applicable and names the flavors of the profile it tried (internal/gating/flavor_stage_test.go). The generated pre-push flavor-audit job runs flavor audit ., so it passes on the same skip. A repository whose markers match a flavor but does not conform still fails, a repository no profile classifies (a mistyped or empty directory, flavor.ErrNoProfile) still fails (cmd/standardsctl/flavor_skip_cli_test.go), and a flavors pin below wins over the skip. flavor apply still refuses with ErrNoFlavorMatched instead of scaffolding a flavor that describes nothing about the repository; pass --flavor=<name> to scaffold or audit against one deliberately. A flat Go module with only root .go files is one such repository.
  • go-service also implements app-service (flavor.MultiProfile), tried after the flavors built for that profile, so a Go service declaring app-service resolves without a flag (internal/flavor/pins_test.go).
  • The flavors list in .standards.yaml pins the flavor and replaces detection for flavor audit, gate run and the generated pre-push hook, which runs the audit. Each entry names a flavor (praetorctl flavor list) and optionally a repository-relative path; a repository with several components lists one entry per path. A path-scoped entry audits the flavor's stack templates and toolchains in that directory and the repository-level files at the repository root, where they live whichever directory holds the stack; the list is the one below (internal/flavor/pins_test.go). An unknown flavor, a missing directory or a path outside the repository, symlinks included, is refused, never skipped. An explicit --flavor still wins on the command line (flavor.ResolveTargets, internal/config/flavor_pins.go, cmd/standardsctl/flavor_pin_cli_test.go).
flavors:
  - name: go-service
    path: api
  - name: frontend-svelte
    path: web

flavor apply, adopt and the Hindsight distiller read the same pin through the same resolver, so they act on the flavor the audit measures. A scaffold writes one flavor at the repository root: with a root pin it scaffolds that flavor. A directory-scoped or a second pin gets no scaffold: flavor apply refuses (flavor.ErrPinNotScaffoldable) and adopt skips the flavor step with that reason and no --flavor suggestion. For such pins, keep the repository-level files (.github/, .vscode/, .paperclip/, lefthook.yml, .gitleaks.toml, .standards.yaml, .standards.lock, AGENTS.md, CLAUDE.md) at the repository root and the stack files (for a Go service go.mod, .golangci.yml, .gosec.json, Dockerfile) under each pinned path; the list is built from the audit's own repository-level lists (flavor.ScopedPinRemedy, internal/flavor/not_applicable_test.go). To scaffold one flavor, pin it once without a path (internal/flavor/pins_apply_test.go). - The Go flavor's Security Scan workflow (templates/go/security-go.yml.tmpl) runs govulncheck and gosec on pull requests (with the draft guard), pushes to main, merge groups and its schedule. It installs both scanners through tools/go/go.mod when that module declares them, and otherwise in a throwaway module pinned to the versions of the engine's own tools module, never @latest, so an adoption pull request fails on the same findings main would (#1371). The flavor's tool list and the security stage give the same pinned install advice for a missing gosec (flavor.GosecInstallGuide), with the golang.org/x/tools floor Go 1.27.2 export data needs. - The Go flavor's tool list prints golangci-lint as go tool -modfile=tools/lint/go.mod golangci-lint run --allow-serial-runners, so a second lint on the same machine waits for the first one's lock instead of failing (#855). - flavor apply also renders the branch ruleset (.github/rulesets/main.json) and lists every other required setting with the command that writes it (flavor settings). It fails when any template or the ruleset could not be written (flavor.ErrApplyIncomplete), and prints what it created before the failure. --force refreshes flavor scaffolds but never rewrites .standards.yaml, .standards.lock or the .workingdir/ ledger (internal/flavor/scaffold_integrity_test.go).

What the flavor score measures

flavor audit scores one thing: the share of the flavor's required templates and settings that the repository carries. The bar is 80%, and a missing template fails the audit at any score. The same commit therefore scores the same on every machine.

Term Scored Checked by
Templates yes the file, or one of its accepted alternatives, exists as a file (a directory of that name does not count)
Settings yes the file exists and, where the setting declares a shape, parses
Toolchains no — advisory exec.LookPath on the machine running the audit
  • Settings are parsed, not counted. lefthook.yml must parse as a non-empty YAML mapping, and for rust-systems, which describes it as clippy and rustfmt enforcement, its pre-commit run lines must call cargo fmt and cargo clippy (internal/flavor/lefthook_setting_test.go); .github/rulesets/main.json and .vscode/settings.json must parse as non-empty JSON objects. The ruleset is strict JSON: comments and trailing commas are rejected, the same line internal/clientsetup draws for client configuration. .vscode/settings.json is JSON with Comments, the format VS Code documents for it: // and /* */ comments and a trailing comma are accepted, so a commented settings file counts as valid (DialectOf in internal/strictjson/dialect.go; internal/flavor/settings_jsonc_test.go). In either format a member name repeated in one object is invalid. A file that is present but does not parse is reported under Missing or Invalid Settings and costs its share of the score: it configures no more than a file that is not there. Settings with no checkable shape are satisfied by presence, which is all that can be claimed about them.
  • The session ledger is not a template. .workingdir/ is git-ignored, so a fresh clone or a linked worktree of a conforming repository never carries it; praetorctl state audit checks the ledger, and flavor audit scores only files the repository tracks (internal/flavor/audit_test.go).
  • Small flavors tolerate no missing setting. With every template present, a repository passes with as many missing or invalid settings as keep it at 80%. That is none for python-ml, os-image, frontend-svelte, jvm-service, mobile-flutter, agentic-autonomous and infra-k8s, and one for every other flavor: python-ml carries 1 template and 1 setting, so one absent .vscode/settings.json scores 50%. The three ledger files used to count as present templates on checkouts that carried them, which bought each of those flavors one more gap (go-service two). TestAuditFlavor_Boundary_SettingsGapTolerancePerFlavor in internal/flavor/audit_test.go pins the number for every flavor.
  • Toolchains never decide pass or fail. They are resolved from PATH, so scoring them measured the auditing machine: a conforming go-service repository scored 11/15 = 73.3% and failed the bar on a host with none of its four tools installed, inside the pre-push hook adoption generates. flavor audit still lists what is missing, with an install command for each, under Missing Toolchains (advisory).

Fixtures pinning the exact scores live in internal/flavor/audit_score_test.go and internal/flavor/settings_audit_test.go.