Skip to content

Commit f079e3d

Browse files
feat: implement the Compass CLI with Nix packaging
~7.5k lines across 16 modules, zero external crates, 162 unit tests. The data model, artifacts, surface, and CLI nodes of the VRS are now realized rather than asserted. - hand-rolled SHA-256, pinned to published vectors - provisional line-based block format (DQ02), hash over rendered bytes - acceptance predicates: recursive-descent all/any/not with branch-level explanations, so readiness can say which part failed (CMP.DM-R15) - head-as-set with divergence, orphans, and per-head-member readiness - admission rejects any file whose content disagrees with its name - build identity via build.rs + CLI_BUILD_STAMP, degrading safely without git so the Nix sandbox builds Verified end to end against a temp catalog: create, revise, evidence, readiness, divergence across two catalogs merged by union, reconciliation, orphan detection, and tamper rejection. Divergence and orphans render differently and name different repairs, which is the distinction the model turns on. Fixes a live bug rather than masking it: style::enabled() read process-wide tty state, so styling_is_inert_when_disabled failed under any pty — Nix's builder and any developer's terminal alike. The rule is now a pure function tested for both states, and the NO_COLOR hermeticity guard in nix/build.nix is removed. Two tests added; the invariant is checked in both directions instead of inferred from the environment. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QzRc44KbA3wfYYKFF9UDMa agent-session-id: cdf2c185-eeb3-4a4d-8ce0-52885f145cac agent-tool: Claude Code agent-tool-version: 2.1.215 agent-model: claude-opus-4-8 agent-runtime-profile: /nix/store/acr8a3l2v366jgmwiq8xdrhgz1py0db5-coding-agent-runtime-profile/share/coding-agents/profile.json agent-skills-manifest: /nix/store/sj1v5j91h8v8d1w9lca4040302lwrd6v-agent-skills-corpus/share/agent-skills/manifest.json tooling-profile: dotfiles@unknown-dirty
1 parent 41c24e1 commit f079e3d

28 files changed

Lines changed: 7783 additions & 0 deletions

.github/workflows/ci.yml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
8+
concurrency:
9+
group: ci-${{ github.ref }}
10+
cancel-in-progress: true
11+
12+
jobs:
13+
nix:
14+
name: nix flake check
15+
runs-on: ubuntu-latest
16+
steps:
17+
- uses: actions/checkout@v4
18+
- uses: cachix/install-nix-action@v31
19+
with:
20+
extra_nix_config: |
21+
experimental-features = nix-command flakes
22+
- run: nix flake check --print-build-logs

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
/target
2+
# prototype scratch catalogs
3+
/tmp

Cargo.lock

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
[package]
2+
name = "compass"
3+
version = "0.0.0"
4+
edition = "2021"
5+
description = "Durable planning intent for coding agents"
6+
license = "MIT"
7+
8+
[dependencies]
9+
10+
[profile.release]
11+
opt-level = 2

build.rs

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
//! Embeds build identity, following the shared build-versioning contract.
2+
//!
3+
//! The canonical stamp is a single-line JSON document under the name
4+
//! `CLI_BUILD_STAMP`, in one of two shapes:
5+
//!
6+
//! ```json
7+
//! {"type":"local","rev":"abc123","ts":1739999700,"dirty":true}
8+
//! {"type":"nix","version":"0.1.0","rev":"def456","commitTs":1739740800,"dirty":false}
9+
//! ```
10+
//!
11+
//! Resolution order at build time:
12+
//!
13+
//! 1. `CLI_BUILD_STAMP` in the build environment — how the Nix packaging layer
14+
//! injects a `nix` stamp. Passed through verbatim.
15+
//! 2. Otherwise a `local` stamp derived from git.
16+
//! 3. Otherwise empty, which resolves to `sourceKind: package` at runtime.
17+
//!
18+
//! **This must never fail the build.** Compass is packaged with Nix, where the
19+
//! build runs in a sandbox with no git and no `.git` directory. Every step
20+
//! here degrades to the next rather than panicking.
21+
22+
use std::process::Command;
23+
24+
fn main() {
25+
println!("cargo:rerun-if-env-changed=CLI_BUILD_STAMP");
26+
// A commit or a dirty working tree changes the stamp.
27+
for p in [".git/HEAD", ".git/index"] {
28+
if std::path::Path::new(p).exists() {
29+
println!("cargo:rerun-if-changed={p}");
30+
}
31+
}
32+
33+
let stamp = std::env::var("CLI_BUILD_STAMP")
34+
.ok()
35+
.filter(|s| !s.trim().is_empty())
36+
.or_else(local_stamp)
37+
.unwrap_or_default();
38+
39+
// Values reach the compiler as an env var, so newlines are not allowed.
40+
let stamp = stamp.replace(['\n', '\r'], " ");
41+
println!("cargo:rustc-env=COMPASS_BUILD_STAMP={stamp}");
42+
}
43+
44+
/// Derive a `local` stamp from git, or `None` when git is unavailable.
45+
fn local_stamp() -> Option<String> {
46+
let rev = git(&["rev-parse", "--short=12", "HEAD"])?;
47+
let ts = git(&["log", "-1", "--format=%ct"])
48+
.and_then(|s| s.parse::<i64>().ok())
49+
.unwrap_or(0);
50+
// An empty porcelain listing means a clean tree. A git failure here is
51+
// reported as clean rather than guessed as dirty.
52+
let dirty = git(&["status", "--porcelain"])
53+
.map(|s| !s.trim().is_empty())
54+
.unwrap_or(false);
55+
56+
Some(format!(
57+
r#"{{"type":"local","rev":"{}","ts":{},"dirty":{}}}"#,
58+
escape(&rev),
59+
ts,
60+
dirty
61+
))
62+
}
63+
64+
fn git(args: &[&str]) -> Option<String> {
65+
let out = Command::new("git").args(args).output().ok()?;
66+
if !out.status.success() {
67+
return None;
68+
}
69+
let s = String::from_utf8(out.stdout).ok()?.trim().to_string();
70+
(!s.is_empty()).then_some(s)
71+
}
72+
73+
fn escape(s: &str) -> String {
74+
s.replace('\\', "\\\\").replace('"', "\\\"")
75+
}

flake.lock

Lines changed: 61 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

flake.nix

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
{
2+
description = "compass - durable planning intent for coding agents";
3+
4+
inputs = {
5+
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
6+
flake-utils.url = "github:numtide/flake-utils";
7+
};
8+
9+
outputs =
10+
{
11+
self,
12+
nixpkgs,
13+
flake-utils,
14+
}:
15+
flake-utils.lib.eachDefaultSystem (
16+
system:
17+
let
18+
pkgs = import nixpkgs { inherit system; };
19+
20+
# Build identity comes from the flake's own source metadata: a clean
21+
# checkout has `rev`, a dirty tree has `dirtyRev`, and a source tree with
22+
# no git at all (a tarball, a `path:` flake) has neither. The binary must
23+
# build in all three cases, so the last one degrades to a named unknown
24+
# rather than failing.
25+
compass = import ./nix/build.nix {
26+
inherit pkgs;
27+
rev = self.rev or self.dirtyRev or "unknown";
28+
commitTs = self.lastModified or 0;
29+
dirty = self ? dirtyRev;
30+
};
31+
in
32+
{
33+
packages = {
34+
default = compass;
35+
inherit compass;
36+
};
37+
38+
devShells.default = import ./nix/rust-dev-shell.nix { inherit pkgs; };
39+
40+
checks = {
41+
inherit compass;
42+
smoke = import ./nix/checks/smoke.nix {
43+
inherit pkgs;
44+
compassPackage = compass;
45+
};
46+
};
47+
48+
formatter = pkgs.nixfmt-tree;
49+
}
50+
);
51+
}

nix/build.nix

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
{
2+
pkgs,
3+
# Build identity for the binary, supplied by flake.nix from the flake's own
4+
# source metadata. See `buildStamp` below.
5+
rev ? "unknown",
6+
commitTs ? 0,
7+
dirty ? false,
8+
}:
9+
10+
let
11+
inherit (pkgs) lib;
12+
13+
cargoVersion = (lib.importTOML ../Cargo.toml).package.version;
14+
15+
# `build.rs` reads `CLI_BUILD_STAMP` and embeds it verbatim; `src/version.rs`
16+
# parses it back out for `compass version`. The Nix sandbox has no `.git` and
17+
# no `git`, so the build script's own git fallback cannot fire — this is the
18+
# only path by which a Nix-built binary learns what it was built from.
19+
buildStamp = builtins.toJSON {
20+
type = "nix";
21+
version = cargoVersion;
22+
inherit rev;
23+
inherit commitTs;
24+
inherit dirty;
25+
};
26+
in
27+
pkgs.rustPlatform.buildRustPackage {
28+
pname = "compass";
29+
version = cargoVersion;
30+
31+
# Keep the source closure tight: the crate and its manifests only. `context/`,
32+
# `.github/`, `target/` and `.git/` are deliberately excluded — nothing in the
33+
# build reads them, and including them would rebuild the package on every doc
34+
# edit.
35+
src = lib.fileset.toSource {
36+
root = ../.;
37+
fileset = lib.fileset.unions [
38+
../Cargo.toml
39+
../Cargo.lock
40+
../build.rs
41+
../rust-toolchain.toml
42+
../src
43+
(lib.fileset.maybeMissing ../tests)
44+
];
45+
};
46+
47+
cargoLock.lockFile = ../Cargo.lock;
48+
49+
env.CLI_BUILD_STAMP = buildStamp;
50+
51+
doCheck = true;
52+
53+
meta = {
54+
description = "Durable planning intent for coding agents";
55+
homepage = "https://github.com/compoundingtech/compass";
56+
license = lib.licenses.mit;
57+
mainProgram = "compass";
58+
platforms = lib.platforms.unix;
59+
};
60+
}

nix/checks/smoke.nix

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
{ pkgs, compassPackage }:
2+
3+
# Smoke-test the built binary the way a user first meets it: it must report a
4+
# version and print help with no catalog, no HOME, and no git checkout in sight.
5+
#
6+
# The version assertions are the real content here. `build.rs` embeds a build
7+
# stamp, and its git-derived fallback cannot fire inside the Nix sandbox — so
8+
# this pins the packaging contract: the stamp Nix injects is the one that comes
9+
# back out, and a build that lost it would report `sourceKind: package` and fail
10+
# here rather than silently shipping an anonymous binary.
11+
pkgs.runCommandLocal "compass-smoke"
12+
{
13+
nativeBuildInputs = [
14+
compassPackage
15+
pkgs.jq
16+
];
17+
}
18+
''
19+
set -euo pipefail
20+
21+
compass --help > help.txt
22+
test -s help.txt || { echo "compass --help printed nothing" >&2; exit 1; }
23+
grep -qi 'compass' help.txt
24+
25+
compass version > version.txt
26+
test -s version.txt || { echo "compass version printed nothing" >&2; exit 1; }
27+
28+
compass version --json > version.json
29+
jq -e '
30+
.sourceKind == "nix"
31+
and (.machineVersion | length > 0)
32+
and (.baseVersion | length > 0)
33+
and (.rev | length > 0)
34+
' version.json > /dev/null || {
35+
echo "compass version --json did not carry the injected nix build stamp:" >&2
36+
cat version.json >&2
37+
exit 1
38+
}
39+
40+
touch "$out"
41+
''

nix/rust-dev-shell.nix

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{ pkgs }:
2+
3+
pkgs.mkShell {
4+
packages = [
5+
pkgs.cargo
6+
pkgs.rustc
7+
pkgs.rust-analyzer
8+
pkgs.rustfmt
9+
pkgs.clippy
10+
]
11+
++ pkgs.lib.optionals pkgs.stdenv.hostPlatform.isDarwin [
12+
pkgs.libiconv
13+
];
14+
15+
env.RUST_SRC_PATH = "${pkgs.rustPlatform.rustLibSrc}";
16+
}

0 commit comments

Comments
 (0)