The compliance floor compiler (ops/compile-floor.sh) transforms governance floor files into enforcement artifacts. It extracts enforcement blocks from Markdown, validates them against a schema, and generates hook scripts, coverage reports, and integrity manifests.
Install gomplate:
brew install gomplate
# or: go install github.com/hairyhenderson/gomplate/v4@latest
# or: download from https://github.com/hairyhenderson/gomplate/releases
# Compile the default floor (reads fleet-config.json for paths)
ops/compile-floor.sh
# Compile all active floors declared in fleet-config.json
ops/compile-floor.sh --all
# Compile a specific floor by name
ops/compile-floor.sh --floor behavioral
# Compile with explicit paths
ops/compile-floor.sh floors/compliance.md .claude/floors/compliance/compiled
# Validate enforcement blocks without writing files
ops/compile-floor.sh --dry-run
# Verify compiled artifacts haven't drifted from source
ops/compile-floor.sh --verify
# Extract enforcement blocks only (no generation)
ops/compile-floor.sh --extract-only
# Generate only prose output
ops/compile-floor.sh --prose-only
# Generate only enforce.sh + semgrep/eslint configs
ops/compile-floor.sh --generate-enforce
# Tag artifacts with a proposal ID
ops/compile-floor.sh --proposal 003
ops/compile-floor.sh Orchestrator — parsing, mode dispatch, context prep
ops/compiler/
├── schema.yaml Declarative enforcement block schema
├── validate.sh Schema-driven block validation
└── templates/
├── enforce.sh.tmpl Hook enforcement dispatcher
├── prose.md.tmpl Floor without enforcement fences
├── coverage.md.tmpl Coverage report table
├── manifest.sha256.tmpl Integrity manifest
├── semgrep-rules.yaml.tmpl
└── eslint-rules.json.tmpl
Floor file (Markdown)
│
▼
┌──────────────────┐
│ preflight_check │ ── Verify environment (manifest, artifacts, source hash)
└──────┬───────────┘
│
▼
┌──────────────┐
│ extract_blocks│ ── Parse ```enforcement fences → block-NNN.yaml files
└──────┬───────┘
│
▼
┌──────────────┐
│ validate.sh │ ── Validate each block against schema.yaml
└──────┬───────┘
│
▼
┌────────────────┐
│ prepare_context│ ── Build JSON context from all blocks
└──────┬─────────┘
│
▼
┌──────────────┐
│ gomplate │ ── Generate artifacts from templates
└──────┬───────┘
│
├── enforce.sh Hook dispatcher script
├── <floor>.prose.md Floor without enforcement blocks
├── coverage report Rule coverage table
├── manifest.sha256 Integrity checksums
├── semgrep-rules.yaml Merged semgrep configs
└── eslint-rules.json Merged eslint configs
Before extraction begins, the compiler runs preflight_check to inspect the compilation environment. This step is diagnostic only – it never blocks compilation, but it logs warnings and emits preflight-remediation metrics events to provide visibility into unexpected state.
What it checks:
manifest.sha256 is presentenforce.sh, <floor>.prose.md) are presentClassification:
| Scenario | Classification | Detail |
|---|---|---|
| First compile (no manifest, no artifacts) | expected | first compile |
| Source floor file changed since last compile | expected | source has changed since last compile |
| Manifest missing but artifacts exist | unexpected | manifest missing but artifacts exist |
| Artifacts missing but manifest exists | unexpected | missing artifacts: <list> |
Output format:
Pre-flight messages are written to stderr with the prefix [preflight]. Warnings use the format:
[preflight] WARNING: Floor '<name>': <description>
[preflight] Floor '<name>': <description>
When it runs:
Pre-flight checks run in compile mode and compile-all mode (via subprocess invocation) only. Other modes (dry-run, verify, extract-only, validate-only, prose-only, generate-enforce) skip pre-flight checks because they do not produce a full artifact set.
Metrics:
Each pre-flight finding emits a preflight-remediation event via ops/metrics-log.sh:
ops/metrics-log.sh preflight-remediation --floor <name> --type <expected|unexpected> --detail "<description>"
Enforcement blocks are YAML embedded in Markdown floor files using fenced code blocks with the enforcement language tag:
### Rule 1
**We MUST NEVER** store secrets in code.
```enforcement
version: 1
id: no-secrets-in-code
severity: blocking
enforce:
pre-tool-use:
type: file-pattern
action: block
patterns:
- '\.env$'
- 'secrets?\.yaml$'
```
| Field | Type | Description |
|---|---|---|
version |
integer | Must be 1 |
id |
string | Unique rule identifier (used in function names and logging) |
severity |
enum | blocking (violations are errors) or warning (violations are advisories) |
enforce |
object | Must contain at least one of pre-tool-use or post-tool-use |
These fields are rejected by the validator to prevent bypass mechanisms: bypass, skip, override.
| Point | When It Runs | Purpose |
|---|---|---|
pre-tool-use |
Before a file edit/write | Block or warn before changes are made |
post-tool-use |
After a file edit/write | Check content after changes |
ci |
Supplementary | Additional CI-only checks (must be paired with pre or post) |
| Type | Valid Points | How It Works |
|---|---|---|
file-pattern |
pre-tool-use only |
Matches file path against regex patterns |
content-pattern |
pre-tool-use, post-tool-use |
Greps file content for regex patterns |
custom-script |
Any | Runs an external script with the file path as argument |
semgrep |
post-tool-use, ci |
Runs semgrep with a rule config file |
eslint |
post-tool-use, ci |
Runs eslint with a rule config file |
Matches the file path being edited against one or more regex patterns.
enforce:
pre-tool-use:
type: file-pattern
action: block
patterns:
- '\.env$'
- 'secrets?\.yaml$'
Greps the file content for regex patterns after an edit.
enforce:
post-tool-use:
type: content-pattern
action: block
patterns:
- 'SSN[:=]\s*\d{3}-\d{2}-\d{4}'
- 'api[_-]?key\s*[:=]\s*["\x27][A-Za-z0-9]{20,}'
Runs an external script. The script receives the file path as its first argument. Exit 0 = pass, non-zero = fail. Scripts run with a 10-second timeout and network isolation (via unshare --net if available).
enforce:
post-tool-use:
type: custom-script
action: warn
script: ops/checks/verify-audit-log.sh
Requirements:
script must be a relative path within the repoRuns semgrep with a rule configuration file. Requires rule-path (must exist) and rule-id.
enforce:
post-tool-use:
type: semgrep
action: block
rule-path: .claude/compliance/semgrep/no-eval.yaml
rule-id: no-eval-calls
Gracefully skips if semgrep is not installed.
Runs eslint with a config file. Only checks JS/TS files. Requires rule-path and rule-id.
enforce:
post-tool-use:
type: eslint
action: block
rule-path: .claude/compliance/eslint/no-any.json
rule-id: no-explicit-any
Gracefully skips if eslint is not installed.
| Severity | Action | Exit Code | Meaning |
|---|---|---|---|
blocking |
block |
2 | Hard stop — edit is rejected |
warning |
warn |
1 | Advisory — edit proceeds with warning |
Contradictions are rejected: warning + block and blocking + warn fail validation.
The main enforcement dispatcher. Called by Claude Code hooks with:
enforce.sh <enforcement-point> <file-path>
Contains:
floors/*.md)compliance-violation and compliance-pass events)The floor file with enforcement blocks stripped — human-readable prose only. Used for context loading (agents read the prose, not the YAML).
A Markdown table showing which rules have automated enforcement and which are judgment-only. Written to COVERAGE_PATH env var (default: docs/compliance-coverage.md).
Checksums for the source floor file and all generated artifacts. Used by --verify mode and the SessionStart hook to detect drift.
Merged config files for semgrep and eslint rules referenced by enforcement blocks.
| Mode | Flag | Description |
|---|---|---|
| Compile | (default) | Full pipeline: extract → validate → generate all artifacts |
| Compile All | --all |
Compile every floor declared in fleet-config.json |
| Dry Run | --dry-run |
Validate and show summary without writing files |
| Verify | --verify |
Compare current hashes against manifest (exit 0 = clean, 1 = drift) |
| Extract Only | --extract-only |
Parse enforcement blocks to YAML files, stop |
| Validate Only | --validate-only |
Extract and validate blocks, don’t generate |
| Prose Only | --prose-only |
Generate only the prose output |
| Generate Enforce | --generate-enforce |
Generate enforce.sh + semgrep/eslint configs only |
When called without positional arguments, the compiler resolves defaults:
fleet-config.json present + jq available: reads .floors.compliance.file and .floors.compliance.compiled_dirfloors/compliance.md exists: uses it with output to .claude/floors/compliance/compiledcompliance-floor.md exists (legacy): uses it with output to .claude/compliance/compiledUse --floor <name> to target a specific floor from fleet-config: --floor behavioral.
The validation schema at ops/compiler/schema.yaml defines structural rules declaratively. The validator (ops/compiler/validate.sh) reads it to check required fields, forbidden fields, and type-per-enforcement-point constraints. Relational constraints (severity/action contradictions, file existence) are code.
To add a new forbidden field: edit forbidden_fields in schema.yaml.
To add a new check type: add an entry to enforce.type_constraints with its valid enforcement points.
Each artifact is generated by a gomplate template in ops/compiler/templates/. The orchestrator builds a JSON context from the extracted blocks and passes it to gomplate as a datasource.
To modify an artifact’s format: edit the corresponding .tmpl file. The template has access to the full context (floor metadata, blocks with enforcement points, stats, hashes).
Template syntax: gomplate documentation.
# Run the full test suite (120 tests)
bash ops/tests/test-compile-floor.sh
# Syntax check
bash -n ops/compile-floor.sh
bash -n ops/compiler/validate.sh
Test fixtures are in ops/tests/fixtures/. To add a test for a new check type, create a fixture floor file and add assertions to test-compile-floor.sh.
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Runtime error (file not found, unknown mode, verify drift detected) |
| 2 | Validation failure or missing dependency (yq, gomplate) |