| name | guardrails-dev-knowledge |
| description | Guardrails repo development knowledge: solution layout, build/test/run commands,
dotnet-tool packaging, testing conventions and gotchas, dogfooding safety rules.
Use when implementing, testing, running, or packaging the harness, or onboarding
an agent to the codebase.
SELF-UPDATING: When your work changes the solution layout, conventions, packaging,
or any fact below, you MUST update the affected section(s) before completing your
task.
|
Guardrails Dev Knowledge
Solution layout
Guardrails.sln # classic .sln (NOT .slnx — CI's .NET 8 SDK can't read it)
global.json # pins SDK 8.0.100, rollForward latestFeature — local == CI
src/Guardrails.Core/ # Model, Loading, Graph, Execution, State, Journal, Prompts — NO UI deps
src/Guardrails.Cli/ # the dotnet tool: PackAsTool, ToolCommandName=guardrails
tests/Guardrails.Core.Tests/ # unit — fake runners/probes, TestData fixture folders
tests/Guardrails.Integration.Tests/ # real processes; plan builders in temp dirs
examples/hello-guardrails/ # golden example: runnable demo + acceptance fixture
.github/workflows/ci.yml # 3-OS matrix: windows/ubuntu/macos-latest
TFM net8.0 everywhere; CLI has <RollForward>LatestMajor</RollForward>.
TreatWarningsAsErrors everywhere. NuGet: System.CommandLine 2.0.9 (GA API:
SetAction/GetRequiredValue), Spectre.Console (CLI only, behind IRunObserver),
YamlDotNet (Core, frontmatter), xunit.v3.
Packaging (dotnet tool)
Commands
dotnet build "Guardrails.sln" -c Release # 0 warnings tolerated
dotnet test "Guardrails.sln" -c Release # full suite; integration spawns real processes
dotnet run --project src/Guardrails.Cli -- validate <plan-folder>
dotnet run --project src/Guardrails.Cli -- run <plan-folder> --no-ui [--fresh]
dotnet run --project src/Guardrails.Cli -- plan|status|reset <plan-folder>
dotnet run --project src/Guardrails.Cli -- graph <plan-folder> [--check] [--stdout] [--format mermaid]
# renders the task/guardrail DAG to <folder>/diagram.md (Mermaid); --check reports staleness
# via a source-sha256 in the file's provenance comment (SSOT §10); --stdout writes nothing
dotnet run --project src/Guardrails.Cli -- skills install [--project] [--target <dir>] [--force]
# `--project` → ./.claude/skills (else ~/.claude/skills); `install skills` is a hidden alias
dotnet pack src/Guardrails.Cli -c Release -o nupkg # local tool package (bundles skills/)
Smoke test of record: run examples/hello-guardrails/hello-guardrails --fresh --no-ui
(spends ~$1 of Claude tokens — prompt tasks; needs claude on PATH).
Conventions & gotchas (hard-won)
- JSON manifests: System.Text.Json with
ReadCommentHandling.Skip +
AllowTrailingCommas — committed examples use // comments; don't break this.
- Diagnostic codes: GR10xx loading, GR20xx validation (
DiagnosticCodes.cs);
tests assert codes; never renumber. GR2009 = prompt-runner command not on PATH
(WARNING, not error). GR2022 = a guardrail/script-action reading a non-ancestor task's
state key (#121); GR2023 = a prompt-runner maxOutputTokens ≤ 0 (#114). GR2025
(WARNING) = plan missing/stale a /guardrails-review marker (#79, SSOT §13) — surfaced
at the CLI command layer (PlanValidator.ReviewMarkerDiagnostic), NOT inside
PlanValidator.Validate. GR2024 (StagingOutputsInvalid) and GR2026 (StaleCoverageToken, the
#157 stale-covers-key-behaviors warning, SSOT §4.4) are both TAKEN; GR2027–GR2030 are now
allocated too — the four-folder/terminal-gate/model block (GR2027 malformed catches: in any of
the four folders; GR2028 terminal <plan>/guardrails/ folder missing a real integration-set re-run;
GR2029 retired integrationGate: true key; GR2030 invalid model value — SSOT §3.3/§2). GR2031
(InvalidAutonomyPolicy — an unrecognized autonomyPolicy value, SSOT §2.1/§7.2; generalized from the
#274 Part C driftPolicy check in the M1 fold, #254/#269/#274) is TAKEN too. GR2032–GR2034 are the
multi-wave block (GR2032 mixed flat/waved layout; GR2033 wave numbering; GR2034 cross-wave dependsOn —
SSOT §14.1). GR2035 (DuplicateCheckName — two checks in ONE folder sharing a Name, e.g.
01-build.ps1+01-build.sh both → Name "01-build"; ERROR, per-folder across all four folders,
#332/SSOT §4.5) and GR2036 (ExpectedDurationNonPositive — a guardrail sidecar's optional
expectedDurationSeconds progress hint ≤ 0, SSOT §4.1.1, issue #331) are TAKEN too. GR2037
(BannedGuardrailPattern — a generated guardrail SCRIPT matches a known-bad regex from the data-driven
banned-pattern registry references/banned-guardrail-patterns.json, embedded into Core + scanned over
the four-folder script guardrails' comment-stripped bodies; ERROR, one per match; seeded #73 + #187a,
#346/SSOT §4.6) is TAKEN too. Next free: GR1010 / GR2038.
- Sorts are ordinal everywhere (guardrail order, task folders) — locale bugs.
- Atomic writes (
AtomicFile) for anything resume reads (state.json, run.json).
- Process spawning:
ArgumentList only; Kill(entireProcessTree: true);
interpreter resolution via InterpreterMap with injectable IExecutableProbe
(tests use FakeExecutableProbe, never the real PATH). Child streams are pinned
UTF-8 (no BOM) in ProcessRunner (stdout/stderr decode + stdin encode) — never
rely on Console.OutputEncoding, which is the Windows OEM code page and corrupts
non-ASCII in the logs (#55, SSOT §5.1).
- Merge-sequence protocol:
journal.ReserveMergeSequence() BEFORE
stateManager.MergeFragment(...); pass the reserved value to RecordAttempt.
- Recorded action outcome → guardrails (
TaskExecutor): a guardrail gets the action's
captured result via GUARDRAILS_ACTION_RESULT (action-result.json = {kind, exitCode, summary}), GUARDRAILS_ACTION_STDOUT, GUARDRAILS_ACTION_STDERR (SSOT §5.1). Doctrine
is verify-don't-replay (#62): a guardrail may verify a postcondition from this recorded
output instead of re-running the action's command — but it's a speed/flake trade-off, sound
only against output the action couldn't fabricate (a produced artifact, a runner-written TRX).
The recorded exitCode is ALWAYS 0 at guardrail time (a non-zero action fails the attempt
first), so never expose a GUARDRAILS_ACTION_EXIT_CODE env var — it would be tautological.
- Claude specifics live ONLY in
Prompts/ — ClaudePromptRunner (flags, invocation),
ClaudeStreamParser (terminal result), and ClaudeTranscriptRenderer (the deterministic
transcript.md projection of the raw stream, #27). Verdicts come from files, never exit
codes. PromptComposer injects dependency-context + prior-attempt pointers that reference
transcript.md (#26); it stays PURE (no IO) — TaskExecutor resolves paths/existence, and
the renderer must stay deterministic (golden-file tested).
- Worktree containment hook (
WorktreeContainmentHook, issues #199/#192, SSOT §9.4): the OUTER
runtime boundary, on top of the write-scope CHECK's post-hoc INNER diff. WriteHookFiles(logDir, worktreeRoot) generates an OS-picked Claude Code PreToolUse hook script (.ps1/.sh, worktree
root baked in as a literal) + a containment-settings.json into the attempt's log dir (never
inside the segment — must not pollute git status); ActionRunner/GuardrailRunner append
--settings <path> to PromptRunnerSettings.ExtraArgs ONLY when a real segment worktree is
present (worktreeRoot param non-null; null in serial mode — no --settings there). The hook's
path-escape decision REUSES WorkspaceContainment.Escapes's rule (re-expressed in shell/PowerShell
since the hook runs as a Claude-spawned OS process, not a .NET callback) — any future change to the
escape rule must be made in WorkspaceContainment.Escapes (unit-tested) AND both script templates
(WorktreeContainmentHookTests runs the REAL generated script standalone with synthetic PreToolUse
stdin JSON via InterpreterMap+ProcessRunner — no claude binary needed — as the drift-catching
regression gate). Same hook additively blocks the git stash family unconditionally in worktree
mode (#192); PromptComposer.ComposeAction/ComposeGuardrail's isWorktreeMode param also injects
a ## Worktree safety advisory section with the stash-free alternative. WorktreeContainmentHookWiringTests
(Integration.Tests) proves the end-to-end plumbing (settings file generated + --settings actually
reaches a fake-CLI's argv) through a real worktree-mode run.
- CLI output seam (
IConsoleIo): the CLI writes ALL user-facing output through an
injected IConsoleIo (Out/Error TextWriters), never the process-global
Console.*. Production wires SystemConsoleIo.Instance (the ONLY place that touches
Console.Out/Console.Error); Program.cs builds the tree via
CommandFactory.BuildRootCommand(io). Every command factory takes io
(ValidateCommand.Create(io), … SkillsCommand.Create(io)) and the helpers take the
writer (ConsoleRunObserver(TextWriter), PlanProbe.PrintDiagnostics(diags, TextWriter),
DryRun.Execute(folder, io), FolderArgument.ResolveAndAnnounce(value, TextWriter)).
LEFT on Console by design: the Spectre LiveRunObserver, the UI-capability probes
(Console.IsOutputRedirected, AnsiConsole...Interactive), and ResetCommand's
confirmation INPUT (Console.IsInputRedirected/ReadLine). Tests inject a
StringWriter-backed StringConsoleIo and capture from its OutText/ErrorText —
no Console.SetOut, no global console state, so the CLI-driving test classes are
parallel-safe (there is no ConsoleCaptureCollection; do not reintroduce one).
- Live status diagram (#219, SSOT §10.1):
OnTheFlyDiagramObserver (in
src/Guardrails.Cli/Ui/, sibling to OnTheFlyLogSiteObserver) is a decorator IRunObserver
wired in RunCommand in BOTH the live and --no-ui paths (stacked AROUND the log-site
observer), writing logs/<runId>/diagram.html — one lock guards map-mutation + atomic write,
best-effort. It translates events to SVG node ids via MermaidRenderer.StatusNodes(plan) (the
status-node surface, sibling to TaskFolderTargets — derived from the SAME AllocateNodeIdBases
OrderBy(Name) ordinal math the emitter uses; a bijection golden test guards drift). The
plan-level bracket badges are driven by concrete PlanGuardrailsStarting/Finished methods
called from RunCommand (NOT IRunObserver methods — keep the interface small). duringRun
toggles the meta refresh + spinner only. One-time fixture gotcha: adding the badge scaffolding
changed the plan-root diagram.html BYTES (a node-status script + badge JS) — regenerate every
committed diagram.html via the real guardrails graph command — but NOT its source-sha256
(status is hash-neutral chrome), so graph --check stays 0 (the golden GraphSourceHash tests
are untouched). One committed docs/plans/preflights-impl fixture is an invalid plan (retired
integrationGate, GR2029) and cannot be regenerated — its diagram.html stays stale.
- Testing doctrine: TCS-gated fakes for concurrency (no sleeps);
.ps1 + .sh
fixture flavors OS-picked; plan builders pin defaultRetries: 0; prompt-pipeline
tests use FakeClaudePlanBuilder (tokenless); real-claude tests gated behind
GUARDRAILS_REAL_CLAUDE=1; xunit.v3 wants TestContext.Current.CancellationToken
(xUnit1051 is an error).
- Windows .sh hazard: bare
bash can resolve to WSL's System32\bash.exe and
fail on Windows paths (GitHub issue #1). Tests/examples use OS-appropriate
scripts; guardrails.json interpreters is the user escape hatch.
Dogfooding safety
When the harness executes a plan that builds THIS repo: guardrail scripts must build
from source (dotnet build/dotnet run --project src/Guardrails.Cli) — never
invoke the globally installed guardrails tool that is executing the plan (file
locks on its own binaries, and you'd be testing the old build).
Design-of-record → draft-PR review workflow (#106)
A design-of-record — a substantial docs/plans/NN-*.md architecture doc — goes into a
draft GitHub PR for inline human review before its implementation milestones begin. The
loop: author the doc on a branch → gh pr create --draft → human comments inline → architect
revises and pushes until addressed → only then does coding (breakdown / harness work) start.
This is the product's own "everything is a reviewable draft a human approves before it runs"
gate applied to the design (docs/plans/01-overview.md pitches the artifacts as "reviewable
in a PR"; plan-breakdown already presents its task folder as a draft).
- Applies to: substantial designs-of-record (new-capability architecture, contract changes,
multi-milestone plans — e.g. the parallel-execution / disjoint-scope plans this loop was
forged on).
- Does NOT apply to: trivial/mechanical changes (typos, one-line clarifications,
renumbering) — those go straight in.
- Distinct from the v2 "CI mode / PR-per-task" roadmap bet #2 (the harness emitting a
check-run/PR per task during a run): that is automation, this is human design review.
The guardrails-architect operating contract owns the full statement (.claude/agents/guardrails-architect.md).
Status pointers
Milestone status lives in guardrails-domain-knowledge → Status section. Roadmap +
Reality Gate: docs/plans/03-roadmap.md. Schema truth: docs/plans/02-schemas-and-contracts.md.