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 ( |
Done (pre-ADR-030) — commit 9563af8 |
3 |
Devstack staleness guard ( |
Done (pre-ADR-030) — commit 9563af8 |
4 |
Wire staleness guard into |
Done (pre-ADR-030) — commit 9563af8 |
5 |
JDM ruleset schema gate ( |
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
.rsfiles (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 -
--forceflag 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 seedcommand — 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 |
|
Cached rebuild |
Dependencies |
|
No-cache rebuild |
Dockerfile |
|
No-cache rebuild |
Compose config |
|
Cached rebuild + compose down first |
Infrastructure |
|
Cached rebuild + compose down first |
Rulesets |
|
Cached rebuild + reseed |
Static assets |
|
Cached rebuild |
Migrations (additive) |
New |
Cached rebuild (applied on startup) |
Migrations (destructive) |
Modified/deleted |
Volume wipe + reseed |
Seed |
|
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 andPRECOMMIT_TOKENworkflow -
local-dev.md: Add "Devstack Staleness Guard" section explaining.devstack/markers, auto-refresh,--forceflag -
testing.md: Add JDM ruleset validation to the pre-push sequence description -
CHANGELOG.adoc: Entry under== Unreleased
Files Touched
| File | Change |
|---|---|
|
New: token-gated 8-question checklist + SPDX check on staged files |
|
New: ~400 lines — staleness detection, marker I/O, migration manifest, 7 unit tests |
|
New: ~100 lines — JDM ruleset schema validation |
|
Add |
|
Register |
|
Register |
|
Wire staleness guard into start/status, add |
|
Add ruleset check step |
|
Add |
|
Pre-commit protocol section |
|
Staleness guard + |
|
Ruleset validation in pre-push sequence |
|
Unreleased entry |
Verification
-
git commitwithout token → checklist displayed, commit blocked -
PRECOMMIT_TOKEN=<token> git commit→ commit succeeds, SPDX checked on staged files -
Stage a
.rsfile without SPDX header → pre-commit rejects after valid token -
Change a
.rsfile →cargo xtask dev statusshows "source code changed, cached rebuild needed" -
Add a migration → status shows "new migration added"
-
Modify a migration → status shows "volume wipe required"
-
cargo xtask dev startwith stale devstack → auto-refreshes with minimum action -
cargo xtask dev start --force→ skips staleness check, full rebuild -
cargo xtask validate→ includes ruleset schema check step -
Corrupt a JDM file → validate fails with serde error path
-
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