Skip to content

Repository-aware editor generation

praetorctl editors generate --path PATH and praetorctl editors verify --path PATH resolve the same repository capabilities before generating or checking editor files. A repository without Go sources or a selected Go profile receives no Go settings, Go inspections or Go problem matcher.

Selecting editors

A repository declares the editors it uses in .standards.yaml:

editors: [vscode, neovim]

praetorctl adopt, onboarding and both editors subcommands resolve that list through editor.SelectEditors (internal/editor/selection.go):

Declaration Result
key absent (or editors: with no value) every supported editor, as before the key existed
a list of ids or aliases exactly those editors
editors: [] no editor; nothing is generated or verified

Every supported editor outside the selection is reported as not applicable: [NOT_APPLICABLE] Editors not selected: ... from the CLI, and a skip action on editors in the adoption report. Its files are neither generated nor verified, so a file the repository deleted is not recreated by the next run. Existing files of an unselected editor are left in place; delete them yourself.

Both subcommands also take --editors=id[,id...], a comma-separated list of editor ids or aliases (cmd/standardsctl/editors.go). A non-empty flag overrides the manifest for that run. Omitting the flag, or passing an empty or all-separator value, falls back to the manifest. Any id that does not resolve, from the flag or the manifest, fails the whole run with unknown editor id(s): <ids>; supported: <canonical ids> and writes nothing: a partially-matched list used to synthesize only the recognized subset and silently drop the rest. Without the flag, a manifest that cannot be read fails the same way instead of falling back to every editor.

Tests: internal/editor/selection_test.go, cmd/standardsctl/editors_selection_test.go, internal/adopt/client_selection_test.go, internal/harvester/onboard_selection_test.go.

Selecting agent clients

agent_clients selects the vendor context files praetorctl compile-context compiles from AGENTS.md and the persona directories it copies .agents/agents/*.md into, with the same absent, list and empty rules as editors:

agent_clients: [claude, codex]
Id Context file Persona directory
claude CLAUDE.md .claude/agents
cursor .cursor/rules/hiss-invariants.mdc none
copilot .github/copilot-instructions.md .github/agents
windsurf .windsurfrules none
gemini .gemini/GEMINI.md .gemini/agents
codex .codex/rules.md .codex/agents

The registry is vendorTargets in internal/agentcontext/render.go. Repository-relative links are rewritten for subdirectory targets by prefixing ../ per directory depth. The compiler reads the key from the manifest beside AGENTS.md, the one that already governs the text register block, so compile-context, compile-context --verify, praetorctl audit, adoption, onboarding and the MCP standards_compile_context tool agree on which projections exist. Persona copies are resolved by compiler.SelectPersonaDirs from the manifest at the root the personas are projected into, which is the same file when AGENTS.md sits at that root. Unselected context files and persona directories print as [NOT_APPLICABLE] and are neither written, verified nor removed, so a directory the repository deletes stays deleted and one it keeps for its own use is left alone. An unknown id fails with unknown agent client id(s): <ids>; supported: <ids>.

The plugin copy under .agents/plugins/praetor/agents belongs to no client and is kept whenever .agents/plugins/praetor/plugin.json exists. Tests: internal/agentcontext/render_selection_test.go, internal/compiler/transpiler_selection_test.go, internal/compiler/agents_selection_test.go, cmd/standardsctl/compile_context_clients_test.go, internal/adopt/client_selection_test.go.

Persona model tiers and client projections

Every canonical agent persona in .agents/agents/*.md declares a model tier key in its front matter (model: flash or model: pro). Missing keys, inherit, or any other values are refused by compile-context, compile-context --verify, and audit with an error naming the file and the allowed values (flash, pro).

Each refusal of a persona that declares no tier names its remedy (adopt.PersonaModelTierRemedy):

  • an unedited copy of an earlier generated persona (repo-auditor.md, repo-gatekeeper.md): a plain praetorctl adopt replaces it with the current template;
  • a generated persona the repository edited: a plain praetorctl adopt adds its template's tier (pro for the auditor, flash for the gatekeeper) as the last front matter line, in the file's own line endings, and keeps every other byte;
  • a persona the repository wrote itself, or one declaring a value outside the tiers: the model line by hand.

TestAdopt_Positive_EditedPersonaGainsModelTier, TestAdopt_Negative_InvalidOrOwnPersonaTierLeftAlone and TestAuditAgentDefinitions_Negative_ModelTierNamesTheRemedy cover the three cases.

  • flash: tier for mechanical runs, fuzzing, dogfooding, and packaging.
  • pro: tier for review and complex reasoning.

Projections carry the tier in each client's own vocabulary or drop it with a stated reason:

Target directory Client Model policy Reason Source URL + read date
.claude/agents Claude Code flash -> haiku, pro -> sonnet Claude Code accepts family aliases sonnet, opus, haiku, fable, full model ID, or inherit https://code.claude.com/docs/en/sub-agents (read 2026-10-14)
.agents/plugins/praetor/agents Antigravity Plugin byte copy, tier kept Native Antigravity schema accepts flash and pro https://antigravity.google/docs/subagents/ (read 2026-10-14)
.github/agents GitHub Copilot model line removed Copilot value space unverified in docs https://docs.github.com/en/copilot/reference/custom-agents-configuration (read 2026-10-14)
.gemini/agents Gemini CLI model line removed Gemini CLI reads .gemini/agents/*.md with a model key, but it takes a concrete Gemini model id (default inherit) and no tier alias, so the tier is dropped and the persona inherits the session model https://github.com/google-gemini/gemini-cli/blob/main/docs/core/subagents.md (read 2026-10-14)
.codex/agents OpenAI Codex model line removed Codex reads standalone TOML files (#1076) https://developers.openai.com/codex/subagents (read 2026-10-14)

The transform edits only the model line inside the leading front matter block; body bytes and line endings are preserved unchanged.

Antigravity IDE

antigravity (aliases agy, antigravity-ide) joins the VS Code-compatible family alongside vscode, cursor and windsurf, and shares their single .vscode/settings.json. Requesting it adds two keys to that file, confirmed against the installed IDE rather than assumed from its docs:

  • antigravity.searchMaxWorkspaceFileCount: 50000 — raises Jetski's per-workspace embedding scan bound above its shipped default of 5,000 (confirmed: contributes.configuration in the IDE's extensions/antigravity/package.json).
  • files.watcherExclude — a core VS Code setting (confirmed: the workbench's own configuration schema) extended with every resolved private directory (editor.Options.PrivateDirs, default .workingdir and .workingdir2), build output (bin, dist) and the isolated gate/dogfood run worktrees under .standards/worktrees, to keep inotify-heavy scratch out of the file watcher.

These two keys are only added when antigravity is part of the requested editor set; vscode/cursor/windsurf-only runs do not receive them. Run-count retention for ~/.local/state/praetor/dogfood-local is a separate, still-open fix tracked on the issue that requested this key.

The shared resolver combines supported project markers and source formats with explicit caller options. It excludes private state, dependency/build directories, nested agent worktrees, and the trees Zig writes inside a checkout: zig-pkg (fetched packages), zig-out and .zig-cache (util.IsToolchainTreeDir). A native build marker adds c and cpp for meson.build or CMakeLists.txt and zig for build.zig or build.zig.zon (needs.NativeLanguages), so a pure-Zig repository gets no C/C++ tooling (internal/editor/native_languages_test.go). cuda comes only from .cu and .cuh sources. An incomplete scan is an error rather than a claim that the unexamined part of the repository has no relevant languages. The scan walks at most 65536 files by default (Options.MaxWorkspaceFiles, zero selects the default, util.DefaultDiscoveryEntries). praetorctl adopt passes the entry bound its verification walk resolved, so --verification-max-entries, or verification.max_entries in .standards.yaml, raises this scan too, up to the same 200000 ceiling. A bound past the ceiling is refused, and adoption names the flag when the scan stops at the bound (large repositories; internal/editor/workspace_bound_test.go, internal/adopt/large_repo_bounds_test.go). praetorctl editors keeps the default.

Every renderer reads the resolved plan and nothing else (Plan in internal/editor/capabilities.go); a capability the resolver rejected is omitted, not asserted anyway:

  • Commands. Default commands include make verify-all only when the reader finds that literal target; a Makefile it cannot read that far offers no command. The Makefile is read by the reader adoption decides verify-all ownership with (util.MakefileHasTarget in internal/util/makefile_target.go, called by hasLiteralMakeTarget in internal/editor/capabilities.go), so a line that only binds a variable, such as verify-all ?= a:b or verify-all: CFLAGS := -g, offers no command, and neither does a rule line inside a define body; adoption verification lists the forms. The reader joins backslash continuations first, as Make does, so the first rule-shaped line below is part of the value of HELP and offers no command, while the continued target list declares verify-all and offers it:
HELP = usage \
  verify-all: run every gate

verify-all \
  other: dep

A computed target name counts once the file fixes its value, so $(NAME): after GATE := verify and NAME := $(GATE)-all offers the command; the adoption guide lists the bindings the reader resolves.

A line the reader claims no rule from offers no command either, although adoption preserves such a file as possibly owning the target: an include, an $(eval ...) call, a pattern target such as verify-%, a target list holding a reference the file does not fix, such as $(PREFIX) verify-all: dep with PREFIX bound nowhere, every rule of a Makefile that names .RECIPEPREFIX (assigning it changes which lines are recipe lines), and every rule at or after the first point the reader cannot resolve: line 4097, a logical line longer than 8192 bytes, a chain of more than 256 continuation lines (257 physical lines), a tab-indented define or conditional whose meaning depends on a conditional branch, or a binding whose computed name may be .RECIPEPREFIX. The reader evaluates no condition, so a verify-all rule inside ifdef CI ... endif offers the command whichever branch Make takes, a known gap the adoption guide lists. The tests are internal/editor/makefile_task_test.go and internal/util/makefile_target_test.go. The generator does not add make build merely because a Makefile exists. VS Code tasks, JetBrains external tools, Neovim user commands, Zed tasks, the Emacs compile-command, Fleet run configurations and Sublime build systems list exactly the resolved commands; an empty plan binds none. Target presence is structural evidence; generation does not run or certify the command. Callers using the Go API can provide explicit commands and languages through editor.Options. - Language server. The Praetor LSP is written only for a Go workspace where the host can start the workspace-relative command editor.Options.LSPPath, or, when that is unset, <BinaryDir>/standards-lsp. Otherwise VS Code settings carry no standards.lsp.* key and Neovim registers no server. The settings name the command, never the file it resolves to, so one tracked .vscode/settings.json verifies on every host. On Linux and macOS the command is an executable regular file. On Windows the VS Code client spawns it without a shell through libuv, whose search_path (src/win/process.c) appends .com and then .exe to a name without an extension: a built bin/standards-lsp.exe starts as bin/standards-lsp. Node rejects .bat and .cmd files in that spawn, so they never count as a language server. Tests: internal/editor/lsp_launch_test.go and cmd/standardsctl/editors_self_gate_test.go. - Extensions. Recommendations are exactly the caller-supplied IDs verified in editor.Options.ExtensionRegistry; the generator does not contact an extension marketplace or prove publication from an extension's source directory. Without that evidence .vscode/extensions.json recommends nothing, golang.go included.

These settings do not prove installation, startup or native client activation. internal/editor/plan_render_test.go covers each rule with evidence present, absent and at its boundary.

Complexity ceilings

The JetBrains inspection profile and the Neovim server settings state the complexity ceilings the repository's policy resolves to, which are the ceilings praetorctl audit enforces there. The JetBrains profile carries the cyclomatic limit (GoCyclomaticComplexity's m_limit) and otherwise names only inspections the IDE provides; it no longer lists HISS01DAGControlFlow, HISS02BoundedLoops, HISS04ComplexityLOC or HISS07ZeroUnwrap, which no plugin implements, so their function-length and statement options were never applied. Those limits reach an editor through standards-lsp. praetorctl adopt uses the policy its session already resolved. praetorctl editors, standards-lsp (for the workspace named in its initialize request) and the MCP standards_inspect_symbols tool (for its server root) use config.ResolveRepositoryComplexity (internal/config/repository_policy.go). A locked repository resolves exactly as praetorctl plan resolves it: pinned profiles, repository overrides and the audit function-length cap.

Every other state resolves to config.HISSComplexityCeiling (cyclomatic 10, cognitive 15, statements 50 and the audit's function length, config.AuditMaxFuncLOC = 60), tightened by any complexity override the manifest declares. Overrides only tighten, so these states never state a limit looser than the ceiling:

Workspace state Result Stated
no .standards.yaml ceiling nothing
manifest, no .standards.lock ceiling tightened by overrides nothing
manifest or lock that does not resolve, including the lock praetorctl init writes ceiling tightened by any readable override [WARN] repository policy unresolved ... (CLI output, MCP report, LSP window/logMessage)

The lock-less row differs from praetorctl plan on purpose. The plan preview shows built-in defaults plus overrides (cyclomatic 15, cognitive 20, 60 lines, 75 statements before overrides); an editor told the looser cyclomatic, cognitive and statement limits would accept what the HISS-04 ceiling rejects. Function length agrees in both: it is hiss.DefaultMaxFuncLOC (internal/hiss/hiss.go), the one constant every function-length default derives from. An unresolvable policy warns rather than failing, because these commands generated editor files before they read policy at all; praetorctl audit still fails on that state. Only a nil or cancelled context fails the resolution: editors and the MCP inspection return the error, and standards-lsp keeps its current ceiling and logs the reason. Tests: internal/config/repository_policy_test.go, cmd/standardsctl/editors_test.go, cmd/standards-lsp/policy_test.go, cmd/standards-mcp/server_test.go.

Reference integrations under editors/

editors/neovim/lua/standards.lua and editors/jetbrains/inspectionProfiles/standards.xml are the Neovim module and JetBrains inspection profile for editors without a Praetor extension. They are generator output: editor.ReferenceSet (internal/editor/reference.go) renders them with the same renderers as editors generate, from a fixed plan (a Go workspace, praetorctl on PATH, bin/standards-lsp, the HISS-04 ceiling), so they do not depend on what the host has built. The Neovim module defines :StandardsAudit, :StandardsCompileContext, :StandardsVerifyAll and :StandardsRatchetSweep; the ratchet sweep runs praetorctl audit, which evaluates the baseline ratchet against the working tree (cmd/standardsctl/audit_ratchet.go).

Never edit them by hand. After changing a renderer, regenerate and check:

make editors-reference          # praetorctl editors reference
make editors-reference-verify   # praetorctl editors reference --verify

make verify-all runs editors-reference-verify, which fails with the drifted file's path. Tests: internal/editor/reference_test.go, cmd/standardsctl/editors_reference_test.go.

The engine's own editor files

The engine checks its own editor files the way an adopter's are checked. Its .standards.yaml selects editors: [universal, vscode], and make verify-all runs make editors-verify (praetorctl editors verify) against that selection:

make editors-verify   # praetorctl editors verify

The gate fails when a selected file is missing or has lost a managed value. The tracked .editorconfig adds two-space sections for the TypeScript, Lua, shell and web sources, so the gate reports it as preserved but unverified. Neovim is not selected: its module is compared byte for byte, and it holds a language-server block only when bin/standards-lsp is built, so a fresh checkout and a built one would disagree. Tests: cmd/standardsctl/editors_self_gate_test.go, which the platform matrix runs on Linux, macOS and Windows.

VS Code extension trace level

To see the messages the extension and the Praetor LSP exchange, open the command palette, run Developer: Set Log Level..., pick the Praetor LSP output channel and choose Trace. The trace then appears in that output channel. At any other log level, including the default Info, nothing is traced.

While the channel is at Trace, the standards.lsp.trace.server setting picks the detail:

Value Traced
messages (default) method names, request ids and response times
verbose the same, plus parameters and results
off the same as messages

To stop tracing, set the channel's log level back above Trace; the setting cannot switch tracing off. vscode-languageclient 10 introduced this gate (refreshTrace in its lib/common/client.js); 9.x read the setting alone, so off disabled tracing and messages traced at every log level.

The extension starts its language client with the id standards.lsp and the name Praetor LSP (LSP_CLIENT_ID and LSP_CLIENT_NAME in editors/vscode/src/setup.ts). The client reads the setting from <client id>.trace.server and names its output channel after the client name. The former id standardsLSP read a key nothing contributes. Tests in editors/vscode/src/setup.test.ts: "the language client reads its trace level from the contributed setting" checks the contributed key and its description; "the locked language client traces only at the Trace log level" runs the locked client's refreshTrace at the Trace, Debug and Info levels.

VS Code extension MCP server

Generated .vscode/settings.json files carry no standards.mcp.* key and no MCP server entry. The extension registers standards-mcp itself through the MCP server definition provider API, defaulting to ${workspaceFolder}/bin/standards-mcp in a trusted workspace; its engine floor is VS Code 1.107. The generated standards.sentinel.headroomMB value is the headroom the extension's Check Sentinel Host Headroom command passes to praetorctl sentinel --min-free-mb. Details and tests: editors/vscode/README.md, sections "MCP server" and "Settings".

Existing configuration

Generation preflights configuration conflicts before writing editor files. JSON configuration retains unrelated user keys and list entries while adding missing managed requirements. A conflicting managed value is reported for review. Verification checks those requirements rather than requiring every JSON byte to match a generated template.

An existing JSON file is read in the format its editor documents for it. VS Code reads .vscode/settings.json, .vscode/extensions.json, .vscode/launch.json and .vscode/tasks.json as JSON with Comments, so in those four files // comments, /* */ comments and a trailing comma are accepted. Every other JSON editor file (.zed, .fleet, .sublime-project) is strict JSON, and a comment there is a syntax error. The rule is DialectOf in internal/strictjson/dialect.go; the flavor audit applies the same rule to .vscode/settings.json (onboarding). In both formats a duplicate key is refused.

  • A commented file that holds every managed value is in sync. editors generate reports it PRESENT and leaves its bytes alone, and editors verify passes it.
  • A commented file that lacks a managed value is never rewritten. A merge decodes the file and writes it again, which would delete every comment. editors generate stops before it writes any editor file, and onboarding before it writes any file, with cannot safely merge existing <path>: it carries comments or trailing commas, which a merge would lose because it rewrites the file, followed by the JSON Pointer of each managed value the file lacks. Add those values by hand, or remove the comments and trailing commas and generate again.

Tests: internal/editor/jsonc_test.go and TestRunEditors_Boundary_CommentedVSCodeFiles in cmd/standardsctl/vscode_jsonc_cli_test.go.

Known legacy Go, unavailable LSP, unpublished extension and invented build-task settings are reported as conflicts when they are no longer supported by the resolved plan. They are retained for review; automatic migration of legacy generated files is not supported. Resolve each reported conflict using the actual repository's languages and commands, then generate and verify again.

Non-JSON formats do not yet have semantic merge adapters. Once they exist, the developer-owned files are never replaced: .editorconfig and .clang-tidy carry hand-tuned project policy, and .idea/workspace.xml, .nvim.lua and .dir-locals.el hold IDE session state or a developer's own editor setup. Generation reports each of them as PRESERVED, and verification lists them as preserved but unverified, because their existence is not semantic verification of XML, Lua or editor Lisp. Any other existing non-JSON file that differs from its template, such as lua/standards.lua or the JetBrains inspection profile, belongs to Praetor and editors generate rewrites it. A file whose only difference is one consistent CRLF line-ending style, as a Windows checkout with core.autocrlf leaves it, is the template: generation reports it PRESENT and verification passes it, while mixed line endings still count as a difference. Missing files are still generated from the resolved plan. The list lives in one place, internal/editor/editor.go (IsPreservedEditorFile).

A successful workspace configuration check does not imply that an IDE extension, coding-agent wrapper, hook or MCP connection is installed and active. Those require the separate client bootstrap and agent lifecycle checks.

Adoption and onboarding

praetorctl adopt resolves an existing editor file with the rule editors generate uses, editor.ResolveExisting in internal/editor/editor.go. Because no audit gate verifies an editor file, adoption keeps differing files, except that it refreshes a recorded, unedited earlier JetBrains inspection profile. The adoption side is internal/adopt/editor_files.go. The editors step runs after the Makefile step (adoptSteps in internal/adopt/adopt.go), so a first adoption's editor files already offer the verify-all target it scaffolds, and a re-run finds them unchanged:

Existing file Plain run --force
identical (CRLF line endings included), or JSON holding every managed value verified verified
unedited earlier Praetor JetBrains inspection profile refreshed to current output; report says Refreshed an unedited earlier refreshed to current output; report says Refreshed an unedited earlier
developer-owned and different preserved preserved
JSON missing managed values kept; the warning names each one as a JSON Pointer, such as "/standards.sentinel.headroomMB" merged: every adopter key and list entry stays, the managed values are added, and the report lists a merge
.vscode JSON missing managed values and carrying comments or a trailing comma kept; the warning names the values to add by hand kept, with the same warning: a merge would lose the comments
JSON adoption cannot merge: a duplicate key, a conflicting managed value, or outside .vscode a comment or a trailing comma kept, with a warning naming the reason and the fix kept, with a warning; adoption continues
any other file that differs, such as lua/standards.lua kept, with a warning and the line delta regenerating would apply kept, with a warning

The JetBrains refresh recognizes only recorded generator output. Its digest fixtures cover every distinct profile shipped from the first release through #476, including the app-service and native-gpu-systems variants; a hand edit keeps the existing file unverified. Current profile output is recorded too, so the boundary test fails when the generator changes without a matching fixture and digest.

A merge re-indents the file with two spaces and sorts its keys. Its report entry carries the line delta and the backup location that every adoption overwrite records (replaced files); a dry run plans the merge and writes nothing.

How a kept file stops warning depends on what it is:

  • Non-JSON file that differs. Delete it and re-run adoption, which regenerates it from the template for the repository's adopted profile. praetorctl editors generate is no substitute here: it renders the framework profile whatever profile adoption selected, so for another profile it rewrites files adoption had verified.
  • A .vscode file with comments or a trailing comma that lacks managed values. Adoption reads the file as JSON with Comments (existing configuration) and keeps it, --force included, because merging would rewrite it without its comments. Add the values the warning names by hand and re-run adoption, which then verifies the file with its comments in place; or remove the comments and trailing commas and re-run adoption with --force to merge the values.
  • A duplicate key, or a comment or trailing comma outside .vscode. Such a file is not valid in the format adoption and praetorctl editors verify read it in, so it never verifies, even when it holds every managed value. praetorctl editors generate refuses it too: it stops with cannot safely merge existing <path> before it writes any file. Remove what the warning names, then re-run adoption with --force to merge the managed values; or delete the file and re-run adoption to regenerate it.
  • A conflicting managed value. praetorctl editors generate refuses it the same way. Set the value the warning names to the managed one, then re-run adoption with --force; or delete the file and re-run adoption.

Onboarding (praetorctl harvest onboard, prepareOnboardEditors and publishOnboardEditors in internal/harvester/onboard.go) applies the editors generate rule itself, under the caller's context and confined to the repository, so a link below it is refused rather than followed:

  • Resolve first. editor.PrepareWritesIn resolves every editor file before onboarding writes anything, dry run included. A commented .vscode file that lacks managed values, like any other file the rule refuses, fails the run with the cannot safely merge existing <path> error and nothing written: no manifest, baseline, AGENTS.md, vendor or editor file. A dry run reports the same error.
  • Merge, keep, create. An existing JSON file keeps every key and gains the missing managed values, a developer-owned file is preserved, and an absent file is created.
  • Publish bound to the read. PreparedWrites.Publish writes each file only while it still holds the bytes it was resolved from, or is still absent, so an edit made in the meantime is refused instead of overwritten. That failure comes after the scaffold is written, so the error says scaffold files have been written and names the command that finishes the run: praetorctl harvest onboard --repo=<path> --dry-run=false.
  • Report. The run prints each file's outcome (CREATED, MERGED, PRESENT, REWRITTEN or PRESERVED) as editors generate prints it, and its plan lists them under editor_files; a dry run lists none.

Before #717 onboarding replaced every existing editor file that was not developer-owned, dropping the repository's own keys and comments.

Tests: internal/adopt/editor_files_test.go, internal/adopt/editor_jsonc_test.go, internal/editor/resolve_existing_test.go, internal/editor/write_in_test.go, internal/editor/prepared_writes_test.go, internal/harvester/onboard_editor_merge_test.go, TestOnboardRepository_Boundary_KeepsDeveloperOwnedNvimLua in internal/harvester/onboard_selection_test.go and cmd/standardsctl/harvest_onboard_editors_test.go.