Skip to content

Develop against this checkout's MCP

Run from the repository root with Python 3, Git, and the Go version required by go.mod available on PATH:

python3 scripts/dev_mcp.py probe

This builds the current Go sources into a temporary binary under the checkout's git-ignored bin/, initializes its real stdio MCP transport, and checks serverInfo.version against provenance.server_version (dev-<source_sha256>, written into main.version with -X, the variable a release build sets too). Reports the standards_dogfood_suite and standards_dogfood_discover tools write record the same string as engine_build.version (ServerOptions.Build in cmd/standards-mcp/server.go). The build stays inside the checkout so the engine build check accepts its context writes even when an untracked file stamps it -dirty. The report includes the checkout, source and binary hashes, Git revision, dirty state, and Go version. Keep that provenance with defect evidence. A source change during the build or probe fails the run; retry against the completed edit.

The probe checks checkout symbol inspection, canonical context compilation with all six outputs read back, verification drift, invalid arguments, path escape, unknown tools, corrupt memory, and malformed locks. It also audits a valid fixture before checking changed pinned content, invariant violations, and incomplete scans that must fail without changing the baseline. All mutations use disposable fixtures. A discovered tool name alone does not prove working behavior.

Native project connections

The MCP server is client-neutral at the wire boundary. Client configuration and lifecycle behavior are separate acceptance surfaces. The supported projections are Codex, Claude, Gemini, OpenCode v1, Continue, Cline, Kilo, and AGY; see agent lifecycle coverage for their current qualification states. A projection test or checked-in client file does not prove that an installed client loaded, trusted, reconnected, or used the server.

Refresh the regular commands on this workstation with make dev-install. make build writes only to the checkout's bin/ directory; it does not update standalone executables already on PATH. scripts/dev_install.py runs the MCP behavior probe against a fresh build, then delegates the atomic install itself to praetorctl workstation install (HISS-19: one installer, not two): it builds the three Praetor binaries and tribunusctl from current source and installs them into ~/.local/bin with the three legacy aliases. With no tracked file modified, current source is a clean clone of HEAD, so untracked files stay out of the build; see install step 4. python3 scripts/dev_install.py --help lists destination overrides for isolated installations.

workstation install rejects an unexpected symlink or special file at an installation target before touching anything, backs up whatever is already there, reads installed hashes back, and restores on a caught installation error. It serializes concurrent installers through a lock directory in the destination and records the install (including the prior commit and backup location, once there is one) in a per-user install manifest. See workstation install and status for the manifest location, the lock and rollback behavior, and workstation status. Already running CLI/MCP processes continue using their original executable until restarted. The development connection below still builds directly from source.

The tracked configs (.codex/config.toml, .mcp.json, .gemini/settings.json) register praetor-dev; they contain no machine-specific paths or credentials. All three run scripts/dev_mcp.py serve, which builds once per server startup. Existing native connections keep that binary after source edits. Restart or reconnect the server before testing the new code, or use the fresh direct commands below. Check the server's initialization version against a fresh probe's provenance.server_version; a matching Git commit alone misses uncommitted edits.

Codex: open this trusted checkout in Codex. Its project .codex/config.toml registers the server with 120-second startup and tool timeouts. The launcher resolves the Git worktree root from the session directory, so nested starts work. Verify the loaded configuration with codex mcp get praetor-dev --json; use /mcp to inspect the connection. Restart the CLI session or IDE extension to load a new server. Project config and these MCP options are documented in the Codex MCP reference.

Claude Code: open the checkout and enable its project .mcp.json server when Claude presents the project approval. Check claude mcp get praetor-dev and use /mcp to reconnect after edits. The launcher expands CLAUDE_PROJECT_DIR inside the child shell; this documented server environment variable avoids depending on Claude's launch directory. Allow cold Go builds with MCP_TIMEOUT=120000 claude; the checked-in per-tool timeout is 120 seconds. See the Claude Code MCP reference.

Configuration parsing was checked with Codex CLI 0.145.0 and Claude Code 2.1.259. Codex's real app-server tool-call path was checked from the repository root and a nested directory. Claude's config was recognized as project-scoped, and its exact launcher completed a handshake and symbol read from an unrelated directory with the documented child environment. Its native connection remains subject to Claude's project approval. If a client cannot load the project connection, record that limitation and use the direct wire client.

Gemini CLI: start Gemini in the checkout root. Its project .gemini/settings.json registers the server as python3 scripts/dev_mcp.py serve with a 120-second request timeout. Gemini reads project settings only from the directory it starts in and launches a stdio server there when no cwd is set, so the relative path always names this checkout's script. Check the loaded server with gemini mcp list and use /mcp to inspect the connection. See the Gemini CLI MCP reference.

Other supported clients: use praetorctl clients prepare or the documented native plan for OpenCode v1, Continue, Cline, Kilo, or AGY. Inspect the exact destination or argv, complete that client's approval/reload flow, and perform the initialization, discovery, and harmless readback checks. Existing projection tests establish serialization and conflict handling; they are not live session evidence.

The adoption MCP tool also reconciles accepted AsDecided ADR records into the project-scoped MCP registry when applying the mcp facet. The generated entries use the same registry schema documented in the decision recall guide; declined decisions are not registered.

Gemini's checked-in .gemini/settings.json also contains the native BeforeTool/AfterTool/AfterAgent lifecycle hooks. Claude's .claude/settings.json contains the corresponding PreToolUse/PostToolUse/ Stop hooks. OpenCode v1, Continue, Cline, Kilo, and AGY have no checked-in Praetor lifecycle adapter; their MCP projection does not imply lifecycle enforcement. Cursor, Windsurf, and Copilot have compiled context support only. See agent lifecycle coverage for the complete matrix.

Reproduce through a real tool

Each direct call fresh-builds the checkout, verifies the server identity, and prints its JSON-RPC response with provenance. Tool errors and protocol errors produce a nonzero exit code:

python3 scripts/dev_mcp.py call standards_inspect_symbols \
  '{"path":"cmd/standards-mcp/main.go"}'

Use a temporary server root for mutation cases. --root follows the subcommand and changes the tool's confined filesystem root; the code still comes from this checkout. This example checks a real write, readback, and verification:

python3 - <<'PY'
import json
from pathlib import Path
import subprocess
import tempfile

with tempfile.TemporaryDirectory(prefix="praetor-mcp-case-") as directory:
    root = Path(directory)
    (root / "AGENTS.md").write_text("# Fixture\n\nPreserve fixture-policy.\n")
    command = ["python3", "scripts/dev_mcp.py", "call", "--root", directory,
               "standards_compile_context"]
    subprocess.run(command + ["{}"], check=True)
    assert "fixture-policy" in (root / "CLAUDE.md").read_text()
    subprocess.run(command + [json.dumps({"verify_only": True})], check=True)
PY

The writing call also splices the text register block into the fixture's AGENTS.md, rendered from the manifest beside it, before the vendor files are compiled. A verify_only call never writes the source: it reports a stale or missing block as Context verification failed, and standards_audit reports it in its context gate. Both also run the caveman lint over AGENTS.md, as praetorctl compile-context --verify does: prose there fails with AGENTS.md fails the caveman lint and the first findings, and a pass names its counts (see the context gate). Both lint every nested AGENTS.md Git tracks the same way, and a prose one fails with nested AGENTS.md fails the caveman lint (nested AGENTS.md files). The writing call runs the same lint after it writes and returns an error on a finding. The fixture above is terse, so it passes.

The block sends agent evidence to register.evidence.dir, by default .workingdir/evidence/. When the root is a Git work tree whose rules do not ignore that directory, the writing call merges the Praetor private-artifact block into .gitignore first and reports it, and a verify_only call fails with git does not ignore <dir> (see evidence). A mktemp -d root outside any work tree needs neither.

The same manifest selects which vendor files exist. agent_clients: [claude] in the fixture's .standards.yaml makes the writing call compile only CLAUDE.md and list the other five as [NOT_APPLICABLE]; a verify_only call then reads only CLAUDE.md. An unknown id fails both calls with unknown agent client id(s). The rule is described in editor and agent client selection.

A read-only tool needs no temporary root. For example, this call lists each client's subagent text states:

python3 scripts/dev_mcp.py call standards_client_capabilities '{}'

Each client entry reports brief_capture, return_capture and register_enforcement as separate states. Each state carries its definition paths and an activation that stays unverified until a payload recorded from the installed native client proves it. The subagent text register gate explains which clients are adapter-defined and which are unenforceable.

For each defect, retain the input, expected behavior, actual response, provenance, and filesystem evidence. Exercise positive, negative, and boundary behavior of the affected tool. Report placeholder results, missing operations, and blocked connections explicitly; the smoke probe does not establish correctness of every tool. Run the relevant code tests and make verify-all after implementing the fix.

Tool arguments and message framing are strict

tools/call refuses every argument key the tool's input schema does not declare, before the handler runs, so a refused call writes nothing. A misspelled preview flag is an error, not a real run with the flag at its default:

python3 scripts/dev_mcp.py call --root "$(mktemp -d)" standards_compile_context \
  '{"verifyonly": true}'

The refusal is a tool result with isError: true that names the undeclared keys and the declared ones, and the command exits nonzero:

mcp: argument not declared by the tool input schema: "verifyonly" (declared: source, target_dir, verify_only)

Every schema in tools/list publishes "additionalProperties": false to match. The check is ToolInputSchema.CheckArguments in internal/mcp/tool.go, called by runTool in cmd/standards-mcp/server.go. The quoted keys are client input, so the refusal passes mcp.SanitizeResult like every handler result: a role delimiter or override phrase in a key is served neutralized. Tests: TestServer_Negative_UndeclaredArgumentRefusedBeforeHandler and TestServer_Negative_MisspelledPreviewFlagWritesNothing in cmd/standards-mcp/server_test.go; TestServer_Negative_InjectedUndeclaredKeyNeutralized and TestServer_Boundary_ManyInjectedUndeclaredKeysNeutralized in cmd/standards-mcp/served_sanitize_test.go.

Every transport reads a message as JSON-RPC 2.0 and answers it as follows:

Message Answer
Not JSON -32700, "id": null
JSON but not one request object: a batch array, a scalar, null -32600, "id": null
id that is null or neither a string nor a number -32600, "id": null
jsonrpc missing or not "2.0", method missing or not a string -32600 with the message's id
No id: a notification nothing, and the method does not run; plain HTTP answers 202 Accepted
Unknown method with an id -32601

Member names are case-sensitive, so {"JSONRPC": "2.0", ...} has no jsonrpc member. A numeric id is echoed exactly, 12345678901234567890 included. Plain HTTP answers a -32700 or -32600 with status 400. The code is decodeRequest in cmd/standards-mcp/transport.go and validateEnvelope in server.go. Tests: TestServer_Negative_EnvelopeValidation, TestServer_Boundary_NotificationsGetNoReplyAndRunNothing, TestStdio_Negative_InvalidMessagesCarryNullOrReadableID, TestStdio_Boundary_NotificationsSilentAndIDsEchoedExactly and TestHTTP_Negative_InvalidEnvelopeAndNotification.

Symbol inspection judges against the repository's ceilings

standards_inspect_symbols prints each Go function's lines, statements, cyclomatic and cognitive complexity beside the ceiling it is judged against, as LOC: 43 (<=60). A function over the length ceiling, which the audit enforces, is marked HISS-04 WARN: LOC. A function over a complexity ceiling is marked HISS-04 REPORT: <kinds> and followed by the [REPORT] lines the audit prints: the scanner's own measurement (hiss.MeasureDecl), reported and never enforced (see HISS-04: Go complexity is measured, not enforced). A function literal bound to a package variable is listed as Func literal. The ceilings come from config.ResolveRepositoryComplexity (internal/config/repository_policy.go) for the server root, the resolver praetorctl editors and standards-lsp also use, so the three report the ceilings praetorctl audit enforces in that repository:

  • A locked repository reports its resolved policy: pinned profiles, repository overrides and the audit function-length cap (config.AuditMaxFuncLOC, 60). That can be looser or tighter than the HISS-04 figures in AGENTS.md.
  • No .standards.yaml, or a manifest without .standards.lock, reports the HISS-04 ceiling (cyclomatic 10, cognitive 15, statements 50, 60 lines), tightened by any complexity override the manifest declares.
  • Every 60 above is hiss.DefaultMaxFuncLOC (internal/hiss/hiss.go), the scanner's own default and the one constant every function-length default derives from; TestServer_Boundary_InspectSymbolsLengthIsScannerDefault in cmd/standards-mcp/server_test.go pins the locked and unadopted cases to it.
  • A manifest or lock that does not resolve, including the lock praetorctl init writes, reports that same ceiling and opens with [WARN] repository policy unresolved (<cause>); stating the HISS-04 ceiling .... The inspection still runs; praetorctl audit still fails on that state.
  • Policy resolution fails the tool only when the request is cancelled or the resolution times out: Inspection failed: resolve complexity policy for <root>: ....

Complexity ceilings explains why the lock-less case differs from praetorctl plan. Tests: TestServer_Boundary_InspectSymbolsFollowsRepositoryPolicy and TestServer_Negative_InspectSymbolsUnresolvablePolicy in cmd/standards-mcp/server_test.go.

standards_plan opens with === Praetor Reconcile Plan (Dry Run) === and takes the Repository: <owner>/<name> line from the manifest's repository block (TestWritePlanHeader_NamesTheManifestRepository). Its target invariants come from config.ResolveRepositoryPolicyFromCatalog, the resolver praetorctl plan uses (createPlanTool in cmd/standards-mcp/server.go), and its drift lines come from adopt.PlanDrift and adopt.FormatPlanStatus (internal/adopt/plan_drift.go), the same code praetorctl plan prints:

  • A locked repository shows the pinned profiles and facets joined with the manifest's overrides, which is the policy praetorctl adopt writes the branch ruleset from. Its drift list follows that policy, so a profile that requires an SBOM reports .github/workflows (no workflow generates an SBOM: …) when no workflow generates one (adopt.SBOMWorkflowDrift, pinned by TestServer_Positive_PlanShowsThePinnedProfilePolicy). Any workflow step that writes an SBOM satisfies that requirement, including a GoReleaser release whose configuration declares sboms; a file named sbom.yml that runs no generator does not, and neither does a step that runs only praetorctl sbom notices, which rewrites THIRD-PARTY-NOTICES.md and writes no SBOM (internal/forge/sbom_workflow_test.go).
  • The tool reads local files only, so its status never says that no change is required: a clean plan reads Status: Local files match the declared policy., and every plan ends with [INFO] Live branch protection not compared with the forge (adopt.PlanLiveNotCompared, TestServer_Positive_PlanAndAuditOnSyncedRepo). praetorctl plan --remote makes that comparison.
  • A manifest without .standards.lock shows the built-in defaults plus the overrides and opens with [INFO] no .standards.lock: built-in defaults and repository overrides only (TestServer_Boundary_PlanOverridesAfterTheJoinAndWithoutALock).
  • A lock that does not resolve, for example a profile edited after it was pinned, fails the tool with Failed to resolve plan policy: ... and prints no policy (TestServer_Negative_PlanRejectsAnUnverifiableLock).
  • A pinned catalog that is not materialized under the server root resolves through the optional catalog_root argument, the confined catalog selection standards_audit takes; a blank value means the server root and a path outside it is refused (TestServer_Boundary_PlanResolvesThroughTheSelectedCatalog).

Each target invariant follows as one - <field>: <value>. line, for example - review_mode: single_maintainer.

Tool text is a counted runtime source

Every tool description, property description and result callsite in cmd/standards-mcp is part of register.sources in .standards.yaml (mcp.descriptions and mcp.outputs). Results go through the helpers in cmd/standards-mcp/mcp_runtime_text.go: a static template is linted as agent text, and runtime-owned text (structured JSON, untrusted passthrough, protocol bytes) is classified and bound by count and digest instead. A bare mcp.TextResult with dynamic text, direct mcp.ToolResult construction or a post-construction mutation fails the extractor.

Adding, removing or rewording a tool string therefore changes the contract. Update expected, not_applicable and sha256 from praetorctl caveman check --configured-sources --root=., and the callsite count and identity digest pinned by TestMCPRuntimeOutputsHaveNoUnclassifiedCallsites in cmd/standards-mcp/runtime_text_coverage_test.go. That test recounts the callsites with an independent AST oracle (cmd/standards-mcp/runtime_output_oracle_test.go) and fails when it disagrees with the production extractor. The form rules are in the text-register guide.

Tool discovery and output offloading

tools/list is byte-stable: tools are sorted by name, each required list is sorted, the server version stays out of it, and initialize declares tools.listChanged: false. TestToolsListGoldenFullMode and TestToolsListGoldenIndexMode pin the bytes against cmd/standards-mcp/testdata/*.golden.json through testsupport.AssertGolden, which folds CRLF; .gitattributes also pins the files to LF. A tool or schema edit fails the tests until the reviewer accepts the new bytes with PRAETOR_UPDATE_GOLDEN=1 go test ./cmd/standards-mcp.

standards-mcp -tools=full (default) lists every tool with its schema, about 18.9 KB. -tools=index lists standards_tools_index, standards_tool_describe, standards_output_read, standards_audit and standards_compile_context, about 3.2 KB. A description above 200 bytes, of a tool or of one property, is cut to its summary in tools/list and ends with a pointer to standards_tool_describe, which returns the full descriptor (TestListedDescriptionsMoveProseToDescribe). The index returns one name | summary | annotations line per tool. tools/call accepts every registered tool in both modes (TestToolsListIndexModeShrinksAndCallStillWorks).

When the text of one result exceeds 16 KiB in all, the largest items are written, after SanitizeResult ran, to .standards/cache/mcp-out/<sha256>.txt (private mode, at most 256 files, oldest removed first) and replaced by one line: [offloaded] path=<repo-relative, forward slashes> bytes=<n> sha256=<hex> read=standards_output_read head=<first 400 bytes, quoted>. Equal bytes give the same file and the same line. standards_output_read {sha256, offset, limit} reads it back, 12 KiB per call, and answers next_offset; only a 64-digit lowercase hex digest names a file, and errors name repository-relative paths only (an evicted pointer says to rerun the tool). A file is served only if the server wrote it (the digest record written.idx in the offload directory lists it), its content matches its digest, and the text is a fixed point of the sanitizer; a committed or hand-placed file that fails any of these is refused (TestReadBackRefusesACommittedCacheFile, TestOffloadReadRefusesAFileTheServerDidNotWrite, TestOffloadReadRefusesNonFixedPointEvenWhenRecorded). Every read-back is sanitized like any other result. A chunk cut inside a phrase the whole text does not contain (xignore previous instructions cut after the x) is therefore served neutralized: the byte fidelity of that chunk is traded for never serving an injection phrase a cut exposed (TestReadBackNeutralizesAPhraseACutExposes). The 4 MiB bound holds for the sanitized bytes too, since neutralizing lengthens text.

The threshold is the mcp.offload_threshold_bytes key of .standards.yaml, read at server start from its mcp section only: absent keeps 16 KiB, 0 turns offloading off (every result is served inline up to 4 MiB), any other value lies in 1024..4194304. An invalid mcp section stops the server. A manifest that is broken elsewhere (an unknown key, a YAML typo) starts the server with the default threshold (the mcp section is still honored where the YAML parses) and a notice on stderr, so standards_audit and standards_adopt stay available to repair it (TestOffloadThresholdFromManifest).

Offloading never leaves files for git to list. The first offload writes .standards/cache/.gitignore containing *, and praetorctl adopt adds /.standards/cache/ to the managed .gitignore block, so an existing block gains the rule on the next run. After creating the cache directory and its ignore file, and before the first output file, the server asks git whether the cache path is ignored; where it is not (an operator-edited ignore file, or no answer from git), no output is written and the result is served inline with an [offload skipped] notice item (TestOffloadRefusesWhereGitDoesNotIgnoreTheCache). A root outside any git work tree needs no such proof; a work-tree lookup that fails (an unreadable ancestor) fails the call instead of skipping the proof. A write failure, such as a read-only root, still withholds the result with an error instead of serving it inline silently; set mcp.offload_threshold_bytes: 0 for a checkout the server cannot write to.

Shared audit authority and parity

The standards_audit tool executes the same gates as CLI standardsctl audit, except the ones its summary line names as CLI-only: the Paperclip harness, the runner matrix, hook activation and the backlog cap gate, which reads the private .workingdir ledgers (Backlog caps; cmd/standards-mcp/audit_tools.go). A failing HISS ratchet reports [FAIL] followed by baseline.RatchetResult.Summary, the rejection text praetorctl audit and the gate print: up to three violations per class by rule, file and line with a count of the hidden rest, or both totals when only the count rose. Like praetorctl audit, the tool first attributes each unbaselined violation against the baseline's commits, commit_sha and the last commit that changed the baseline file (hiss.AttributeRatchet), so a finding in code unchanged since then is tagged as coming from a changed check, not as new (A HISS rejection names the violations). The tool takes no listing flag; praetorctl baseline --verify --all-violations prints every violation. README presentation findings never share the HISS baseline: the tool's README gate reads .readme-baseline.json on its own, and with that file absent and enforcement off it reports the findings as measured, never as a ratchet pass. standards_adopt reports one verdict per baseline file, in the order .standards-baseline.json, .gosec-baseline.json, .readme-baseline.json, and keeps the rejection line of a kept baseline as the CLI prints it (README presentation baseline). The tool keys a finding as the CLI does (hiss.ConvertToBaseline): by rule, file and the function that holds it, never by its line, so a baselined function that only moved passes the tool's ratchet as well (A baseline entry survives a line shift). Both tools share the same authority implementations for artifact checks. When verifying branch protection rulesets, standards_audit consults adopt.AuditBranchProtectionWithPolicy with the effective policy it resolved (profiles, facets and overrides joined), the policy adopt and sync render the ruleset from. If .standards.yaml records an accepted adoption.decline: [branch-ruleset], standards_audit reports a verified pass ([PASS] Branch protection ruleset declined by adoption.decline.) rather than failing on the absent .github/rulesets/main.json. When not declined and required by policy, standards_audit fails closed if the ruleset file is missing or its content differs from the ruleset the declared policy renders for the repository's default branch (forge.ValidateRepositoryRuleset, branch from forge.RepositoryDefaultBranch), matching CLI behavior with byte-for-byte verdict parity (cmd/standards-mcp/audit_branch_ruleset_test.go).

The context, label taxonomy and hook configuration gates read adoption.decline through the reader the CLI gates use, adopt.AuditDecline, and the label and hook gates are the CLI's own (adopt.AuditLabelTaxonomy, adopt.AuditGitHookConfig). The hook gate accepts lefthook.yml, or without it a .pre-commit-config.yaml that runs both praetor commands; it reads no installed hook (the hook runner the audit accepts). A declined labels or git-hooks step passes with the decline named; a declined agent-harness step still requires the register block, the projections and the caveman lint, and a failure says so (declined steps, cmd/standards-mcp/audit_decline_test.go).

The HISS-11 supply-chain gate is the CLI's own too (adopt.AuditSupplyChain, run with the effective policy): it fails when the declared SLSA level, enforce_cosign or require_sbom exceeds what the workflow files measure (How the audit measures the SLSA level, TestServerAuditRunsTheSupplyChainGate in cmd/standards-mcp/audit_policy_test.go). So is the HISS-10 build-warnings gate (adopt.AuditBuildWarnings): it fails a workflow lane that compiles C, C++, Rust or Go code without its toolchain's warnings-as-errors form, and skips a repository with no such lane (Build-warnings gate, TestServerAuditRunsTheBuildWarningsGate).

The release-token gate is the CLI's own too (adopt.AuditReleaseToken, run with the manifest's exceptions list): it fails a release-please step that takes its token from anything but an earlier, unconditional GitHub App token step of the same job, and skips a repository with no release-please step (Release token gate, TestServerAuditRunsTheReleaseTokenGate in cmd/standards-mcp/audit_policy_test.go).

The HISS-18 workflow trigger check is the CLI's own as well (forge.AuditWorkflowTriggers, run with the manifest's exceptions list). Its findings are [WARN] lines and the gate still passes; only a workflow it cannot read or a malformed HISS-18 exceptions entry fails it (Workflow trigger audit, TestServerAuditRunsTheWorkflowTriggerCheck in cmd/standards-mcp/audit_policy_test.go).

The test-timeout gate runs right after it (forge.AuditTestTimeouts, #1381): a workflow line that sets a timeout above the 30-minute per-package bound fails the audit unless a live test-timeout exceptions entry names the workflow; an expired entry or one that excuses nothing fails too (TestServerAuditTestTimeoutsLikeTheCLI in cmd/standards-mcp/audit_test_timeouts_test.go). The audit summary counts it, so a clean fixture reads passed: 12/12. A reusable workflow's -timeout=${{ inputs.<id> }} is judged by that input's workflow_call default; an input without a literal default, or a default that is no duration, is an unreadable timeout and fails (TestAuditTestTimeouts_Positive_InputDefaultWithinBound, TestAuditTestTimeouts_Negative_InputDefaultAboveBound in internal/forge/test_timeout_audit_test.go).

The job-timeout gate runs right beside it (forge.AuditJobTimeouts): every job that runs steps declares timeout-minutes. A reusable workflow call (uses: at job level) cannot carry it and is exempt; an expression value (${{ ... }}) is reported as not judged, by name. The gate has two paths: the static path warns on any declared value above 120 minutes; when the forge is readable, the measured path reads up to the latest 3 successful runs on the default branch and warns on a declared value below 1.25 x slowest run S (cancel risk) or above max(30, 4 x S) (runaway), falling back to the static path for jobs without measurements. During warn-only rollout, findings emit [WARN] lines and the gate passes, silenced by a dated job-timeout exception.

The context gate inside standards_audit also runs the cache-stability check (compiler.VerifyStableContext, called from auditContextSync in cmd/standards-mcp/audit_tools.go), the same one praetorctl audit and compile-context --verify-stable run. It renders AGENTS.md twice under different injected clocks and target visit orders and scans the head band for volatile text (a timestamp with seconds, a sha256: digest, an absolute path, a run or build counter). A drifting render or a volatile head fails the tool call with [FAIL] Agent context cache stability: ... naming the token. An AGENTS.md without band markers still passes; the tool prints the unlayered [WARN] line in the context gate's output, after the [PASS] Agent context ... line (Context cache bands, TestMCPAuditAgentContext_Negative_RefusesVolatileHeadBand and TestMCPAuditAgentContext_Boundary_UnmarkedSourceWarns in cmd/standards-mcp/audit_context_stable_test.go).

The lock digest gate inside standards_audit resolves its catalog from the same catalog_root tool argument the effective-policy gate uses (p.policy.CatalogRoot in cmd/standards-mcp/audit_tools.go), not the repository root by default. It calls config.ValidateLockfileWithOptions with RequireSources: true, so a lockfile with no catalog to hash against fails the tool call ([FAIL] lockfile content digests are unverifiable: ..., config.ErrLockUnverifiable) instead of reporting a pass. A catalog that exists but no longer defines a pinned archetype's id: fails the same way with config.ErrLockSourceMissing. See lock verification outcomes for the full outcome table shared with the CLI.

Adoption and version-audit parity

standards_adopt and praetorctl adopt render one outcome and one set of governance pillar lines, both derived from the adoption report (AdoptReport.Outcome and AdoptReport.Pillars in internal/adopt/report.go). Both also pass the same forge collaborator counter (adoptCollaborators in cmd/standards-mcp/tools_adoption.go): with GITHUB_TOKEN or GH_TOKEN in the server's environment, a first adoption of a GitHub origin with exactly one writer declares review_mode: single_maintainer, as the adoption guide describes; without a token the server reaches no forge. A dry run that recorded an error is reported [INCOMPLETE], not [SIMULATED (DRY RUN)], and a pillar whose step warned, failed (by returning an error or by recording one), was declined or never ran names that status instead of a success mark. A dry run against a fresh repository without source_root shows both: the DevContainer pillar reads warned and the result is [INCOMPLETE]. standards_adopt and praetorctl adopt also list a file that force overwrote in its own replaced section, with its line delta and backup location, and never among the reconciled files (formatAdoptFiles in cmd/standards-mcp/tools_adoption.go, TestFormatAdoptMCPResult_Positive_ReplacedSection). The force and source_root property descriptions restate, in the agent register, the --force contract adopt --help prints: what force rewrites, merges and keeps, and that it needs source_root, a dry run included (what a forced re-adoption changes, TestCreateAdoptTool_ForceStatesTheContract in cmd/standards-mcp/tools_adoption_force_test.go). source_root accepts what --lock-source-root accepts: a Praetor Git checkout or a source bundle. A source holding go.mod must sit in a Git checkout, because the DevContainer bootstrap lists it with git ls-files. The tool refuses one that does not before anything is written, with source_root: prepare devcontainer bootstrap: bootstrap source "<path>": not a Git checkout; .... Only a failure to capture the source names the parameter; every other error keeps its own subject (devcontainer.NameSource, TestAdoptMCPSourceRootRefusalNamesParameter_3D in cmd/standards-mcp/tools_adoption_source_test.go).

record_baseline records a first .standards-baseline.json and keeps an existing one. On a repository that already has the file the tool prints Existing Legacy Debt Baseline: <n> infractions and, below it, the ratchet verdict on the kept file, the line praetorctl adopt prints (adopt.BaselineRatchet.Line; TestFormatAdoptionBaselineStatesAndDryRunLabels in cmd/standards-mcp/tools_test.go). A rejecting verdict stays in that line and in a [WARN] entry that lists the findings: the tool prints no closing line, where praetorctl adopt ends with not ready yet: Debt Baseline in place of its success line. The tool has no argument that re-records an existing baseline; that is praetorctl adopt --rerecord-baseline or praetorctl baseline --record (the baseline on a re-adoption).

A dry run also prints the branch ruleset preview the CLI prints: the action (create, update, unchanged or keep), its note, and the rendered ruleset or the diff. Both print adopt.FilePreview.Text (internal/adopt/preview.go; TestFormatAdoptMCPResultPrintsPreviews in cmd/standards-mcp/report_renderers_test.go). The adoption guide explains the actions.

The facets: line names the facets the run applies, the declared ones for an existing .standards.yaml. When a first adoption falls back to the default facets, each AdoptReport.FacetNotes line follows it as facet note: <note>, the notes the CLI prints under Facets: (TestFormatAdoptMCPResultPrintsFacetNotes). The adoption guide shows them under 1-Step CLI Adoption.

standards_version_audit lists the workflow-action inventory that praetorctl bump audit prints, rendered by the same bump.FormatActionsInventory, and counts it in its summary line (TestFormatVersionAuditListsActions in cmd/standards-mcp/report_renderers_test.go). The summary line ends with passed:, and a report that failed, one holding any deprecation, comes back as an error result, the verdict on which bump audit exits non-zero (TestVersionAuditResultFollowsPassed). The audit queries upstream registries, so exercise it through a real call only where network access is intended.

Context compilation parity

standards_compile_context runs the code praetorctl compile-context runs: compileContext in cmd/standards-mcp/server.go calls compiler.VerifyCompiledContext for verify_only and compiler.CompileContextProjections for a write (internal/compiler/projection.go). A tool call and the CLI therefore check and write the same things:

Canonical personas in .agents/agents/*.md may declare an optional tools list. Its closed engine vocabulary is read, search, edit, shell, web, and task; omitted tools keeps the current projection unchanged. Unknown, duplicate, or empty lists fail with the persona path and allowed vocabulary. The compiler translates those classes in internal/compiler/agent_tools.go:

Client Native projection Tool names or permission keys Checked source/version (2026-10-17)
Claude Code .claude/agents/*.md Read; Grep, Glob; Edit, Write, NotebookEdit; Bash; WebFetch, WebSearch; Agent, TaskCreate, TaskGet, TaskList, TaskUpdate, TaskStop Installed Claude Code 2.1.302; subagent reference
GitHub Copilot .github/agents/*.md read; search; edit; execute; web; agent, todo Custom agents configuration, checked 2026-10-17
Gemini CLI .gemini/agents/*.md read_file, read_many_files; grep_search, glob, list_directory; write_file, replace; run_shell_command; web_fetch, google_web_search; invoke_agent Subagents, main checked 2026-10-17; Gemini CLI executable is not installed in this environment
Codex .codex/agents/*.md No per-agent tool allow-list. Projection removes the canonical tools field, inserts a gap comment, and audit reports each affected persona. Installed Codex CLI 0.162.1; subagent reference, checked 2026-10-17
OpenCode .opencode/agents/*.md permission map: default "*": deny; allow read; grep, glob, list; edit; bash; webfetch, websearch; task for the corresponding classes Installed OpenCode 1.18.7; agent permissions
Antigravity No verified persona-file projection No per-agent tool mapping verified; no Antigravity persona directory is registered by internal/agentcontext/render.go Installed desktop app 2.23.0; Antigravity CLI transition announcement, 2026-05-19. That announcement confirms subagents but does not define a persona file schema.

Claude, Copilot, and Gemini rows are emitted as native tools allow-lists. OpenCode’s mapping is implemented for its documented front matter format but OpenCode is not yet a registered agent_clients projection. Codex and Antigravity gaps are explicit: a persona declaration does not claim enforcement for those clients. The Codex projected Markdown file retains a visible HTML comment because current Codex custom agents use standalone TOML files; the existing Codex projection is not a verified native agent-file format.

  • A write compiles the vendor files and the canonical read-only context projection (AGENTS.readonly.md), then copies every persona under .agents/agents into each selected client's persona directory and, when .agents/plugins/praetor/plugin.json exists, into the plugin's agents/ and skills/ copies.
  • verify_only also runs the caveman lint over every persona and skill and fails on a persona or plugin skill copy that differs from its source beyond leading and trailing whitespace. It runs every check and returns every failure. A vendor file or copy in one consistent line-ending style compares as its LF form, so a Windows checkout (CRLF under * text=auto) verifies; one with mixed line endings compares byte for byte and the failure says so (projectionMatches in internal/compiler/agent_projection.go, tests in internal/compiler/checkout_line_endings_test.go).
  • A write runs the same lint after writing and fails on a finding, and before it compiles it makes Git ignore the evidence directory (register.evidence.dir). Both steps are adopt.CompileAgentContext, the write the CLI's compile-context and init run.
  • More than 50 files in .agents/agents fail both modes instead of being truncated.
  • Every file the call writes below target_dir (the vendor files, AGENTS.readonly.md, the persona copies such as .claude/agents/*.md, and the plugin persona and skill copies), and every persona and skill it reads from .agents, is reached without following a symlink. A symlinked file, or a symlinked directory anywhere between target_dir and the file (.claude, .agents, .agents/plugins/praetor), is refused on write and on verify for the same reason, with or without -allow-outside-root. That flag still admits a target_dir outside the root; it no longer lets a link redirect a write. The writer itself (contextopt.WriteSnapshotIn) creates and opens every directory below target_dir without following a link, so a link planted after the check is refused too.
  • A write reads every persona and skill, then checks every file it is about to write before it splices the text register into source and before it writes the first file (planAgentSurfaces in internal/compiler/projection.go). The check applies the writer's own refusals: the symlink refusals above, an existing file that is not a regular file, and an existing file the writer cannot observe because it is not UTF-8 text, holds a NUL byte or exceeds 1 MiB (contextopt.MaxSourceBytes). Any of those leaves source, the vendor files, the persona copies and the plugin copies unchanged. The check does not cover an I/O failure during the writes themselves, such as a full disk, or a target changed between the check and the write: the writer applies the same refusals again when it reaches each file, but the files written before a refusal stay written.
  • Before a write, both check that the running engine matches the engine checkout they write into (workstation.CheckBuildCurrent, internal/workstation/freshness.go). An installed server or CLI built from an older revision refuses the write with engine build does not match this checkout instead of rendering its own stale text into AGENTS.md and the vendor files; verify_only is never refused. The rules are in the engine build check.

Tests: cmd/standards-mcp/server_projection_test.go, cmd/standards-mcp/compile_context_engine_test.go, TestCompileContextRejectsSymlinkedOutputDescendants and TestCompileContextWritesRealOutputDescendants in cmd/standards-mcp/server_path_test.go, internal/compiler/output_paths_test.go, internal/compiler/projection_test.go and internal/contextopt/write_in_test.go.

standards_adopt and praetorctl adopt write the vendor files of the selected clients and the two canonical personas (repo-auditor.md, repo-gatekeeper.md) through the same writer, and project the personas with compiler.CompileAgentSurfaces, the persona half of the compile-context write, plugin persona and skill copies included. Before the first adoption step writes anything, adopt runs the same check over those files and every persona and plugin copy (preflightAgentSurfaces in internal/adopt/adopt.go), so a symlinked .agents or persona directory fails adoption with nothing written, and so does a symlinked .github when copilot is a selected client (its vendor file lives there). An existing selected vendor file or persona that is not UTF-8 text, holds a NUL byte or exceeds 1 MiB is refused instead of overwritten. A dry run runs the check too; a step declined through adoption.decline is not checked. Every other file adoption writes, AGENTS.md, the pull request template and the workflows included, still goes through the older writer (writeRepoFile), which refuses a link that leaves the repository but follows one that stays inside it: with copilot unselected, a symlinked .github fails later in the run, after files were written behind it. Tests: internal/adopt/agent_surface_preflight_test.go, internal/adopt/plugin_projection_test.go.

standards_audit runs the persona, skill and nested AGENTS.md caveman lint the CLI audit runs (compiler.AuditAgentSources), but it does not verify persona or skill projections yet; see the persona and skill gate.

Needs reports read operator settings at call time

standards_needs_report selects the operator settings on every call, the way praetorctl hook does: PRAETOR_FLEET_CONFIG and PRAETOR_WORKSTATION_CONFIG, then the install manifest (loadNeedsRegistry in cmd/standards-mcp/server.go). It loads the settings and the targets' contracts through needs.SelectRegistry, the loader the praetorctl needs subcommands use (TestSelectRegistry_3D in internal/needs). A settings change therefore reaches a running server without a restart. The tool resolves framework.targets and its framework argument exactly as praetorctl needs report does (needs.SelectFrameworkSource) and prints the same header (needs.FormatReportHeader, including Deprecated input: lines). Praetor ships no framework data: with no target configured the header reads Framework: not configured (…) with mapping availability n/a (no target framework configured). A settings document that fails validation is an error result (Failed to load operator settings: ...), not a report against defaults (TestNeedsReportHonoursConfiguredTarget in cmd/standards-mcp/operator_settings_test.go):

PRAETOR_WORKSTATION_CONFIG=/path/to/workstation.yaml \
  python3 scripts/dev_mcp.py call standards_needs_report '{"path":"."}'

See framework targets for what a target changes in a report.

The tool scores the repository through needs.ReportRepoWithFramework, as praetorctl needs report does, and prints the same umbrella-import recommendations (needs.FormatUmbrellaImports) when the repository imports an umbrella package of the selected go framework (TestNeedsMCPReportRecommendsUmbrellaSubPackages in cmd/standards-mcp/needs_coverage_test.go).

Retained public dogfood loops

Use the public dogfooding guide for the shared CLI/MCP plan/apply/recheck loop. Public cloning is disabled by default. Explicitly add --allow-remote-benchmarks to serve or call; a client tool argument cannot turn on network access. Direct calls can use --timeout 300 for the bounded five-minute loop. Keep artifact_dir under the confined server root, such as .workingdir/evidence/public-dogfood, and retain the JSON-RPC envelope with its source identity.

For recurring suites, use the user timer guide. standards_dogfood_schedule_status reads a configured schedule and retained attempt state. The configured runner binary, suite, source bundle, state, and repair-policy paths must all fit the server root. Status cannot run suites, dispatch agents, or create state. It hashes the configured CLI executable so MCP and CLI status agree even though they are separate processes.

For configured repair execution, use the repair guide. standards_dogfood_repair_status reads a retained suite report and execution state through a confined private configuration. Probe it with an actual failed suite report, then verify that status leaves both the report and absent execution state unchanged. It never invokes credential helpers or providers. Only the explicit praetorctl dogfood repairs run command starts an execution attempt.

The private execution configuration must pin source_sha to a commit that exists in the configured source repository and carry the canonical register tuple and manifest digest for that snapshot. Repair status rejects stale or caller-selected provenance before it reports retained execution state.

Package docs quote their upstream

standards_package_docs returns the sheet praetorctl docs sync distilled from a declared package's own documentation (CompressDocumentation in internal/docdistill/compressor.go). That documentation is third-party text, so the sheet quotes it rather than presenting it as guidance:

  • a provenance line under the header says the quoted lines are upstream claims, not instructions;
  • the summary, configuration lines and upstream notes are > quotations, and the API surface is code spans that a backtick in the harvested text cannot close;
  • a carriage return or other control character in a harvested line becomes a space, so no harvested text starts a line of the sheet.

Treat a quoted line as the package author's claim and check it before acting on it. The rendering is pinned by the TestQuotedNotes_* tests in internal/docdistill/docdistill_test.go and, through the tool, by TestServer_PackageDocs_QuotesUpstreamText in cmd/standards-mcp/tools_test.go.

The catalog behind the tool uses schema v2 (CatalogVersion in internal/docdistill/cache.go). A v1 catalog was rendered before quoting, so its sheets are not served: the tool answers not-found, and praetorctl docs audit lists the package as stale, until praetorctl docs sync harvests it again. A catalog with an unknown version is refused with ErrCatalogVersion rather than overwritten. docs audit counts a package as documented only when its sheet extracted content (DistilledDoc.Extracted).

Rule explanations state when a rule is not enforced

explain_rule answers for every HISS identifier, including the ones with no executable check. Those answers say so explicitly:

Rule: HISS-05 (Variable Scoping)
Enforcement: NOT ENFORCED. No executable check exists in this repository, and no configured
linter decides this rule.
Failure Action: None today; the rule is advisory until a check is attached.
Adopted repositories: not enforced; adoption generates no check for this rule.

The answers come from the HISS rule catalog in internal/hisscatalog/catalog.go. The generated wiki's HISS Matrix (docs/wiki/HISS-Matrix.md) and the invariant table of every adopted AGENTS.md render the same catalog, so the tool, the wiki and an adopted harness cannot disagree about which invariants exist.

An agent asking about a rule needs to know whether anything will stop it. Returning a formal specification with no enforcement note reads as a gate that exists, which is the defect the HISS-20 coverage catalog was built to remove — a declared mechanism nothing implements. The same honesty applies here: the tool reports the rule and whether it bites.

A rule enforced for some languages and not others names the split rather than one mechanism for all of them. HISS-01 answers per language: Go decides goto, direct recursion and cycles between plain functions; Rust, Python, JavaScript, TypeScript, Svelte and shell decide direct recursion only; C and C++ decide goto only, less the forward cleanup gotos a declared hiss.exceptions.c_goto_cleanup accepts (declared HISS exceptions). Each line matches a claim in .config/hiss/coverage.yaml, and TestServer_ExplainRuleHISS01ScopesEnforcementPerLanguage in cmd/standards-mcp/server_test.go fails if the answer drifts back to a universal claim.

The Enforcement line describes praetor's own mechanism. Every answer ends with an Adopted repositories: line that states what an adopted repository gets instead: the check and the generated stages that run it, or not enforced; adoption generates no check for this rule. For HISS-01 the line starts 'praetorctl audit' HISS scan in verify-all + lefthook pre-commit/pre-push: Go 'goto', recursion + plain-function call cycles; Rust, Python direct recursion; C 'goto'. The tool answers for no particular repository, so it names every pipeline a full adoption generates and adds that the check holds only where adoption generated that pipeline. An adopted AGENTS.md table credits only the pipelines its own run generated. Both read the Adoption field of the same catalog entry (internal/hisscatalog/adopted.go), so they cannot disagree (TestAdoptedExplanation_Positive_StatesAdoptedEnforcement and TestAdopted_Boundary_FollowsGeneratedPipelines in internal/hisscatalog/adopted_test.go).

Package docs answer the same way twice

standards_package_docs resolves a package name through one selection rule, shared with praetorctl docs lookup (DocCatalog.Lookup, internal/docdistill/types.go):

  1. an exact package_name match wins;
  2. a path-suffix match (yaml.v3 for gopkg.in/yaml.v3) answers only when nothing matches exactly, so a suffix can never beat a package the caller named in full;
  3. when the catalog holds the same package at several versions, the greatest catalog key (name@version) in sorted order wins. That order is lexical, not semantic: probe a specific version by asking for the catalog entry rather than expecting semver ordering.

Before this rule the tool ranged the catalog map, so two calls against one cache could return different versions and a suffix match could win over an exact one (internal/docdistill/catalog_lookup_test.go pins all three points).

The fact tallies printed by standards_hindsight_optimize and by praetorctl hindsight distill|audit are sorted by category for the same reason: two runs over an unchanged repository produce identical output, so a diff between them shows a real change.

Hindsight distillation names the sources it could not read

standards_hindsight_optimize and praetorctl hindsight distill harvest facts from four sources (workspaceSources, internal/hindsight/distiller.go):

Source On failure
bug ledger (.workingdir/BUGS.md) required: the run fails and the cache is not written
flavor (flavor.Resolve) optional: recorded as a warning
dedupe scan optional: recorded as a warning
package docs (.workingdir/docs/catalog.json) optional: recorded as a warning

Each optional failure lands in DistillationReport.Warnings with the source name, and the remaining sources still run. The MCP tool then heads its result Partially distilled instead of Successfully distilled, and both surfaces print one Warning: line per failed source. When sources failed and nothing was harvested, the run is an error, so an empty result never replaces a populated .workingdir/memory/distilled.json.

Tests: internal/hindsight/distiller_warnings_test.go, TestServer_HindsightOptimizeNamesFailedSources (cmd/standards-mcp/tools_test.go) and cmd/standardsctl/hindsight_cli_test.go.

Issue claim tools write to the forge

standards_issue_claim, standards_issue_status and standards_issue_release are open-world, non-read-only tools: they edit a claim comment, labels and assignees on a GitHub issue. They decode their arguments into the forge.ClaimCommand that praetorctl issue claim, status and release build and run it on the same forge.ClaimDesk, so a probe through the tool and a CLI call give the same result. The token comes from GITHUB_TOKEN or gh auth token; the stale window comes from forge.claim_stale in the operator settings. Never point a probe at a live issue: exercise them through the test seam (newClaimDesk, a fake forge) as cmd/standards-mcp/tools_issue_claims_test.go does. The protocol is in issue claims.

A claim on a closed issue is refused and names the issue's state; reopening stays a deliberate act. A release removes the assignee the claim added, and keeps an assignee who was on the issue before the claim; the claim comment records which case applies (#1292).