Plan: Devstack Hardening — Pre-commit Protocol, Staleness Guard, Ruleset Gate

On this page

Status

Step Description Status

1

Plan document, GitLab issue, branch

Done (pre-ADR-030)

2

Pre-commit hook (.githooks/pre-commit) — token-gated 8-question checklist + SPDX check on staged files

Done (pre-ADR-030) — commit 9563af8

3

Devstack staleness guard (xtask/src/devstack_guard.rs) — SHA-256 marker files, minimum-action detection, auto-refresh (536 LOC, 8 unit tests — plan originally said 7)

Done (pre-ADR-030) — commit 9563af8

4

Wire staleness guard into dev.rs — start/status integration, --force flag

Done (pre-ADR-030) — commit 9563af8

5

JDM ruleset schema gate (xtask/src/cmd/rules.rs) — compile all rulesets via zen-engine at validate time

Done (pre-ADR-030) — commit 9563af8

6

Documentation updates (coding-conventions, local-dev, testing, CHANGELOG)

Done (pre-ADR-030) — plan-hygiene sweep 2026-04-17

Issues: #174
Branch: chore/devstack-hardening

Context

Three quality and operational gaps were identified by comparing CRAIG with Canopy’s xtask infrastructure:

No pre-commit gate. Shortcuts — weakened tests, missing documentation, untracked TODOs — are only caught at push time (15+ minute pre-push battery) or by human review after the fact. During the March 2026 session, every quality issue flagged by the user (weakened assertions, deferred tests, stale docs) would have been caught by a pre-commit reflection checklist. Canopy implemented a token-gated 8-question protocol that forces the committer to address these before creating the commit.

No devstack staleness detection. After code changes, developers must guess whether to run dev reload (preserve data, cached rebuild), dev restart (wipe data, full rebuild), or nothing. Getting it wrong wastes time: unnecessary restarts take 2+ minutes, while skipping a needed restart causes test failures against stale containers. Canopy’s staleness guard hashes source code, dependencies, Dockerfile, migrations, and seed data, then determines the minimum Docker action needed — from "skip entirely" to "volume wipe + no-cache rebuild."

No pre-push JDM ruleset validation. Invalid or malformed JDM rulesets are caught by integration tests (which compile and evaluate them), but not by the fast cargo xtask validate step. A broken ruleset JSON passes fmt/clippy/build and only fails 10 minutes into the nextest run. A lightweight schema check (deserialize + construct Decision) takes <1 second and catches structural regressions immediately.

Scope

In scope:

  • Pre-commit hook with token-gated reflection checklist (8 questions)

  • SPDX header check on staged .rs files (fast, per-commit)

  • Devstack staleness guard with SHA-256 marker files in .devstack/

  • Minimum-action detection: None / CachedRebuild / NoCacheRebuild / VolumeWipe

  • Migration manifest tracking (additive vs destructive changes)

  • Integration with dev start, dev status, dev reload

  • --force flag to bypass staleness check

  • JDM ruleset schema validation in cargo xtask validate

  • Unit tests for staleness guard (7 tests) and ruleset validation

Out of scope:

  • Ephemeral port allocation — separate plan at ephemeral-port-allocation.adoc

  • Standalone cargo xtask seed command — CRAIG’s seed runs as a Docker container; decoupling is a separate effort

  • Pre-commit hook customization per developer — the checklist is standardized

Design

Pre-commit Protocol

The hook generates a random token (openssl rand -hex 8), stores it in .git/.precommit-token, and displays the 8-question checklist. The commit is blocked until the developer sets PRECOMMIT_TOKEN=<token> git commit, proving they read the checklist. On valid token, SPDX headers are checked on staged .rs files only (via git diff --cached --name-only).

The pre-commit hook is fast (<1 second). It does NOT run fmt, clippy, or tests — those remain in pre-push.

Staleness Guard

Adapted from Canopy’s devstack_guard.rs. Hashes are computed over:

Category Paths Rebuild if changed

Source code

services/, crates/, tools/ (.rs files)

Cached rebuild

Dependencies

Cargo.toml, Cargo.lock

No-cache rebuild

Dockerfile

Dockerfile

No-cache rebuild

Compose config

docker-compose.yml

Cached rebuild + compose down first

Infrastructure

devstack/

Cached rebuild + compose down first

Rulesets

rulesets/

Cached rebuild + reseed

Static assets

services/craig-web/static/, services/craig-intake/static/

Cached rebuild

Migrations (additive)

New .sql files

Cached rebuild (applied on startup)

Migrations (destructive)

Modified/deleted .sql files

Volume wipe + reseed

Seed

tools/craig-seed/src/, rulesets/

Reseed only

Markers are stored in .devstack/ (gitignored). Written after successful dev start. Read by dev status (report only) and dev start (auto-refresh).

Ruleset Gate

Walks rulesets/georgia/.json and rulesets/texas/.json, deserializes each to zen_engine::model::DecisionContent, and constructs a zen_engine::Decision. Parameter files (no nodes array) are skipped. Any failure reports the serde error path and exits non-zero.

Added as a step in cargo xtask validate after SPDX headers and before cargo fmt.

Steps

Step 1: Plan document + GitLab issue + branch

Files: docs/modules/ROOT/pages/plans/devstack-hardening.adoc, docs/modules/ROOT/nav.adoc

Create this plan document. Link in nav.adoc under Active Plans. Create GitLab issue #174. Branch: chore/devstack-hardening.

Step 2: Pre-commit hook

Files: .githooks/pre-commit

Create the hook script (~50 lines bash): - Token generation: openssl rand -hex 8.git/.precommit-token - Token validation: compare $PRECOMMIT_TOKEN env var to stored token - On valid token: check SPDX headers on staged .rs files via git diff --cached --name-only --diff-filter=ACM — '*.rs' - On missing/invalid token: display 8-question checklist, exit 1

Step 3: Devstack staleness guard

Files: xtask/src/devstack_guard.rs, xtask/Cargo.toml

New module (~400 lines) implementing: - StalenessReport struct with rebuild, volume, seed, reasons fields - check_staleness() — reads markers, compares to current hashes, returns report - write_markers() — writes all hashes to .devstack/ - clear_markers() — removes .devstack/ directory - markers_exist() — checks if markers are present - Migration manifest: collect_migration_manifest(), check_migration_state() — additive vs destructive detection - Display impl for human-readable staleness report - RebuildType::max() for priority ordering (None < Cached < NoCache)

Add sha2 = "0.10" to xtask Cargo.toml.

Unit tests (7): - rebuild_type_max_ordering — None < Cached < NoCache - staleness_report_display_current — "up to date" message - staleness_report_display_stale — shows reasons + action - migration_manifest_roundtrip — serialize/parse identity - migration_state_no_change — no change = no wipe - migration_state_new_file_additive — new file = changed but no wipe - migration_state_modified_file_wipes — modified = wipe - migration_state_deleted_file_wipes — deleted = wipe

Step 4: Wire into dev.rs

Files: xtask/src/cmd/dev.rs, xtask/src/main.rs

Modify start(): - If markers exist and --force not set: call check_staleness(), print report, execute minimum action - If markers don’t exist or --force: full start (current behavior) - After successful start: call write_markers()

Modify status(): - If markers exist: call check_staleness(), print report - Otherwise: show current container status only

Add --force flag to DevCommand::Start.

Register devstack_guard module in main.rs.

Step 5: JDM ruleset schema gate

Files: xtask/src/cmd/rules.rs, xtask/src/cmd/mod.rs, xtask/src/cmd/validate.rs, xtask/Cargo.toml

New command module (~100 lines): - Walk rulesets/georgia/.json and rulesets/texas/.json - Skip files without nodes array (parameter files) - Deserialize to DecisionContent, construct Decision - Report pass/fail count with error paths

Add zen-engine to xtask Cargo.toml (workspace dependency).

Add as step in validate.rs after SPDX headers, before fmt:

println!("[N/M] Checking JDM rulesets");
cmd::rules::check()?;

Register rules module in cmd/mod.rs.

Step 6: Documentation

Files: .claude/docs/coding-conventions.md, .claude/docs/local-dev.md, .claude/docs/testing.md, CHANGELOG.adoc

  • coding-conventions.md: Add "Pre-commit Protocol" section explaining the 8 questions and PRECOMMIT_TOKEN workflow

  • local-dev.md: Add "Devstack Staleness Guard" section explaining .devstack/ markers, auto-refresh, --force flag

  • testing.md: Add JDM ruleset validation to the pre-push sequence description

  • CHANGELOG.adoc: Entry under == Unreleased

Files Touched

File Change

.githooks/pre-commit

New: token-gated 8-question checklist + SPDX check on staged files

xtask/src/devstack_guard.rs

New: ~400 lines — staleness detection, marker I/O, migration manifest, 7 unit tests

xtask/src/cmd/rules.rs

New: ~100 lines — JDM ruleset schema validation

xtask/Cargo.toml

Add sha2 and zen-engine dependencies

xtask/src/main.rs

Register devstack_guard module

xtask/src/cmd/mod.rs

Register rules module

xtask/src/cmd/dev.rs

Wire staleness guard into start/status, add --force flag

xtask/src/cmd/validate.rs

Add ruleset check step

.gitignore

Add .devstack/

.claude/docs/coding-conventions.md

Pre-commit protocol section

.claude/docs/local-dev.md

Staleness guard + .devstack/ section

.claude/docs/testing.md

Ruleset validation in pre-push sequence

CHANGELOG.adoc

Unreleased entry

Verification

  1. git commit without token → checklist displayed, commit blocked

  2. PRECOMMIT_TOKEN=<token> git commit → commit succeeds, SPDX checked on staged files

  3. Stage a .rs file without SPDX header → pre-commit rejects after valid token

  4. Change a .rs file → cargo xtask dev status shows "source code changed, cached rebuild needed"

  5. Add a migration → status shows "new migration added"

  6. Modify a migration → status shows "volume wipe required"

  7. cargo xtask dev start with stale devstack → auto-refreshes with minimum action

  8. cargo xtask dev start --force → skips staleness check, full rebuild

  9. cargo xtask validate → includes ruleset schema check step

  10. Corrupt a JDM file → validate fails with serde error path

  11. cargo nextest run -p xtask → 7 staleness guard unit tests pass

Documentation Updates

  • .claude/docs/coding-conventions.md — pre-commit protocol section

  • .claude/docs/local-dev.md — staleness guard, .devstack/ markers

  • .claude/docs/testing.md — ruleset validation step

  • CHANGELOG.adoc — entry under == Unreleased

Edit this page · latest