From 0524488a73fdce925a8ad675077c2968174f1469 Mon Sep 17 00:00:00 2001 From: Goudarz Firoozi <63324407+greygoody@users.noreply.github.com> Date: Thu, 23 Jul 2026 20:22:37 +0330 Subject: [PATCH 01/24] feat(nix): add target checkpoint and resume contracts --- crates/codectx-core/src/nix.rs | 662 +++++++++++++++++++++++++++++++++ 1 file changed, 662 insertions(+) create mode 100644 crates/codectx-core/src/nix.rs diff --git a/crates/codectx-core/src/nix.rs b/crates/codectx-core/src/nix.rs new file mode 100644 index 0000000..0d51e89 --- /dev/null +++ b/crates/codectx-core/src/nix.rs @@ -0,0 +1,662 @@ +use crate::{ + find_repo_root, sha256_bytes, sha256_file, CodectxError, CommandGitProbe, GitProbe, PathRule, + Result, +}; +use camino::{Utf8Path, Utf8PathBuf}; +use serde::{Deserialize, Serialize}; +use std::{fs, path::Path, process::Command}; + +pub const NIX_CHECKPOINT_SCHEMA_VERSION: u32 = 1; +pub const DEFAULT_NIX_SPEC_PATH: &str = ".codectx/nix.toml"; +pub const DEFAULT_NIX_CHECKPOINT_PATH: &str = ".codectx/checkpoints/nix/latest.json"; + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct NixTargetSpec { + pub id: String, + pub target: String, + #[serde(default = "default_flake_lock_path")] + pub flake_lock_path: Utf8PathBuf, + #[serde(default)] + pub baseline: Option, + #[serde(default)] + pub expected_paths: Vec, + #[serde(default)] + pub supporting_paths: Vec, + #[serde(default)] + pub constraints: Vec, + #[serde(default)] + pub checks: Vec, +} + +fn default_flake_lock_path() -> Utf8PathBuf { + Utf8PathBuf::from("flake.lock") +} + +impl NixTargetSpec { + pub fn load(repo_root: &Utf8Path, path: &Utf8Path) -> Result { + let resolved = resolve_path(repo_root, path); + if !resolved.exists() { + return Err(CodectxError::MissingNixSpec(path.to_path_buf())); + } + + let contents = fs::read_to_string(resolved)?; + let spec: Self = toml::from_str(&contents)?; + spec.validate()?; + Ok(spec) + } + + pub fn validate(&self) -> Result<()> { + if self.id.trim().is_empty() { + return Err(CodectxError::InvalidNixSpec( + "id must not be empty".to_string(), + )); + } + if self.target.trim().is_empty() { + return Err(CodectxError::InvalidNixSpec( + "target must not be empty".to_string(), + )); + } + Ok(()) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct NixRepositoryState { + pub root: Utf8PathBuf, + pub branch: String, + pub head: String, +} + +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum NixPathRelevance { + Expected, + Supporting, + Unexpected, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct NixChangedPath { + pub path: Utf8PathBuf, + pub relevance: NixPathRelevance, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct NixObservation { + pub repository: NixRepositoryState, + pub target: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub flake_lock_digest: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub current_system: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub candidate_system: Option, + pub changed_paths: Vec, +} + +impl NixObservation { + pub fn input_fingerprint(&self) -> Result { + Ok(sha256_bytes(serde_json::to_vec(self)?)) + } + + pub fn unexpected_paths(&self) -> Vec { + self.changed_paths + .iter() + .filter(|change| change.relevance == NixPathRelevance::Unexpected) + .map(|change| change.path.clone()) + .collect() + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct NixEvidence { + pub kind: String, + pub status: String, + pub input_fingerprint: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub command: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub output_ref: Option, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)] +pub struct NixCheckpointNotes { + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub confirmed_facts: Vec, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub unresolved_hypotheses: Vec, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub next_probe: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct NixCheckpoint { + pub schema_version: u32, + pub spec: NixTargetSpec, + pub observation: NixObservation, + #[serde(default)] + pub notes: NixCheckpointNotes, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub evidence: Vec, +} + +impl NixCheckpoint { + pub fn new(spec: NixTargetSpec, observation: NixObservation) -> Self { + Self { + schema_version: NIX_CHECKPOINT_SCHEMA_VERSION, + spec, + observation, + notes: NixCheckpointNotes::default(), + evidence: Vec::new(), + } + } + + pub fn digest(&self) -> Result { + Ok(sha256_bytes(serde_json::to_vec(self)?)) + } + + pub fn to_pretty_json(&self) -> Result { + let mut json = serde_json::to_string_pretty(self)?; + json.push('\n'); + Ok(json) + } + + pub fn load(path: &Utf8Path) -> Result { + let contents = fs::read_to_string(path)?; + let checkpoint: Self = serde_json::from_str(&contents)?; + if checkpoint.schema_version != NIX_CHECKPOINT_SCHEMA_VERSION { + return Err(CodectxError::UnsupportedNixCheckpointVersion( + checkpoint.schema_version, + )); + } + Ok(checkpoint) + } + + pub fn write(&self, path: &Utf8Path) -> Result<()> { + if let Some(parent) = path.parent() { + fs::create_dir_all(parent)?; + } + fs::write(path, self.to_pretty_json()?)?; + Ok(()) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct NixInvalidation { + pub code: String, + pub reason: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct NixStaleEvidence { + pub kind: String, + pub reason: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct NixResumeAssessment { + pub checkpoint_digest: String, + pub current: NixObservation, + pub retained_context: Vec, + pub invalidated_context: Vec, + pub scope_drift: Vec, + pub stale_evidence: Vec, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub next_probe: Option, +} + +pub trait NixProbe { + fn evaluate_toplevel( + &self, + repo_root: &Utf8Path, + target: &str, + ) -> Result>; + fn current_system(&self) -> Result>; +} + +#[derive(Debug, Clone, Copy, Default)] +pub struct CommandNixProbe; + +impl NixProbe for CommandNixProbe { + fn evaluate_toplevel( + &self, + repo_root: &Utf8Path, + target: &str, + ) -> Result> { + let output = Command::new("nix") + .arg("eval") + .arg("--raw") + .arg(target) + .current_dir(repo_root) + .output()?; + + if !output.status.success() { + return Err(CodectxError::NixCommandFailed { + args: format!("eval --raw {target}"), + stderr: String::from_utf8_lossy(&output.stderr).trim().to_string(), + }); + } + + let path = String::from_utf8(output.stdout)?; + let path = path.trim(); + if path.is_empty() { + Ok(None) + } else { + Ok(Some(Utf8PathBuf::from(path))) + } + } + + fn current_system(&self) -> Result> { + let path = Path::new("/run/current-system"); + if !path.exists() { + return Ok(None); + } + let resolved = fs::canonicalize(path)?; + Ok(Some( + Utf8PathBuf::from_path_buf(resolved).map_err(CodectxError::NonUtf8Path)?, + )) + } +} + +#[derive(Debug, Clone)] +pub struct NixContextBuilder { + git_probe: G, + nix_probe: N, +} + +impl Default for NixContextBuilder { + fn default() -> Self { + Self::new(CommandGitProbe, CommandNixProbe) + } +} + +impl NixContextBuilder +where + G: GitProbe, + N: NixProbe, +{ + pub fn new(git_probe: G, nix_probe: N) -> Self { + Self { + git_probe, + nix_probe, + } + } + + pub fn inspect(&self, start: impl AsRef, spec: &NixTargetSpec) -> Result { + spec.validate()?; + let repo_root = find_repo_root(start)?; + let git = self + .git_probe + .read_state(&repo_root, spec.baseline.as_deref())?; + let lock_path = resolve_path(&repo_root, &spec.flake_lock_path); + let flake_lock_digest = if lock_path.exists() { + Some(sha256_file(&lock_path)?) + } else { + None + }; + let changed_paths = classify_nix_paths( + &git.changed_files, + &spec.expected_paths, + &spec.supporting_paths, + ); + + Ok(NixObservation { + repository: NixRepositoryState { + root: git.root, + branch: git.branch, + head: git.head, + }, + target: spec.target.clone(), + flake_lock_digest, + current_system: self.nix_probe.current_system()?, + candidate_system: self.nix_probe.evaluate_toplevel(&repo_root, &spec.target)?, + changed_paths, + }) + } +} + +pub fn assess_nix_resume( + checkpoint: &NixCheckpoint, + current: NixObservation, +) -> Result { + let mut retained_context = vec![ + format!("change id {}", checkpoint.spec.id), + format!("target {}", checkpoint.spec.target), + ]; + retained_context.extend( + checkpoint + .spec + .constraints + .iter() + .map(|constraint| format!("constraint: {constraint}")), + ); + + let mut invalidated_context = Vec::new(); + if checkpoint.observation.target != current.target { + invalidated_context.push(NixInvalidation { + code: "target_changed".to_string(), + reason: format!( + "checkpoint target {} differs from current target {}", + checkpoint.observation.target, current.target + ), + }); + } + if checkpoint.observation.repository.head != current.repository.head { + invalidated_context.push(NixInvalidation { + code: "repository_head_changed".to_string(), + reason: format!( + "repository HEAD changed from {} to {}", + checkpoint.observation.repository.head, current.repository.head + ), + }); + } + if checkpoint.observation.flake_lock_digest != current.flake_lock_digest { + invalidated_context.push(NixInvalidation { + code: "flake_lock_changed".to_string(), + reason: "flake.lock changed; prior evaluation, build, test, and closure-diff evidence must be repeated" + .to_string(), + }); + } + if checkpoint.observation.candidate_system != current.candidate_system { + invalidated_context.push(NixInvalidation { + code: "candidate_system_changed".to_string(), + reason: "the evaluated NixOS system store path changed".to_string(), + }); + } + if checkpoint.observation.current_system != current.current_system { + invalidated_context.push(NixInvalidation { + code: "active_system_changed".to_string(), + reason: "the host active system changed since the checkpoint".to_string(), + }); + } + + let current_fingerprint = current.input_fingerprint()?; + let stale_evidence = checkpoint + .evidence + .iter() + .filter(|evidence| evidence.input_fingerprint != current_fingerprint) + .map(|evidence| NixStaleEvidence { + kind: evidence.kind.clone(), + reason: "evidence was recorded against a different Nix input fingerprint".to_string(), + }) + .collect(); + let scope_drift = current.unexpected_paths(); + let next_probe = next_probe(&invalidated_context, &scope_drift, ¤t); + + Ok(NixResumeAssessment { + checkpoint_digest: checkpoint.digest()?, + current, + retained_context, + invalidated_context, + scope_drift, + stale_evidence, + next_probe, + }) +} + +pub fn classify_nix_paths( + paths: &[Utf8PathBuf], + expected_paths: &[String], + supporting_paths: &[String], +) -> Vec { + let expected_rules: Vec = expected_paths.iter().cloned().map(PathRule::new).collect(); + let supporting_rules: Vec = supporting_paths + .iter() + .cloned() + .map(PathRule::new) + .collect(); + + let mut classified: Vec = paths + .iter() + .cloned() + .map(|path| { + let relevance = if expected_rules.iter().any(|rule| rule.matches(&path)) { + NixPathRelevance::Expected + } else if supporting_rules.iter().any(|rule| rule.matches(&path)) { + NixPathRelevance::Supporting + } else { + NixPathRelevance::Unexpected + }; + NixChangedPath { path, relevance } + }) + .collect(); + classified.sort_by(|left, right| left.path.cmp(&right.path)); + classified +} + +pub fn render_nix_observation(spec: &NixTargetSpec, observation: &NixObservation) -> String { + let mut output = String::new(); + output.push_str("CODECTX · nixos inspect\n\n"); + output.push_str(&format!("Change: {}\n", spec.id)); + output.push_str(&format!("Target: {}\n", observation.target)); + output.push_str(&format!("Repository: {}\n", observation.repository.head)); + output.push_str(&format!( + "flake.lock: {}\n", + observation.flake_lock_digest.as_deref().unwrap_or("missing") + )); + output.push_str(&format!( + "Active system: {}\n", + observation + .current_system + .as_deref() + .map(Utf8Path::as_str) + .unwrap_or("not observed") + )); + output.push_str(&format!( + "Candidate system: {}\n", + observation + .candidate_system + .as_deref() + .map(Utf8Path::as_str) + .unwrap_or("not evaluated") + )); + + let unexpected = observation.unexpected_paths(); + output.push_str(&format!("Unexpected paths: {}\n", unexpected.len())); + for path in unexpected { + output.push_str(&format!(" - {path}\n")); + } + output +} + +pub fn render_nix_resume(assessment: &NixResumeAssessment) -> String { + let mut output = String::new(); + output.push_str("CODECTX · nixos resume\n\n"); + output.push_str(&format!( + "Checkpoint: {}\n", + assessment.checkpoint_digest + )); + + if assessment.invalidated_context.is_empty() { + output.push_str("Invalidated context: none\n"); + } else { + output.push_str("Invalidated context:\n"); + for invalidation in &assessment.invalidated_context { + output.push_str(&format!( + " - {}: {}\n", + invalidation.code, invalidation.reason + )); + } + } + + if !assessment.scope_drift.is_empty() { + output.push_str("Scope drift:\n"); + for path in &assessment.scope_drift { + output.push_str(&format!(" - {path}\n")); + } + } + + if !assessment.stale_evidence.is_empty() { + output.push_str("Stale evidence:\n"); + for evidence in &assessment.stale_evidence { + output.push_str(&format!(" - {}: {}\n", evidence.kind, evidence.reason)); + } + } + + output.push_str(&format!( + "Next probe: {}\n", + assessment.next_probe.as_deref().unwrap_or("none") + )); + output +} + +fn next_probe( + invalidations: &[NixInvalidation], + scope_drift: &[Utf8PathBuf], + current: &NixObservation, +) -> Option { + if invalidations + .iter() + .any(|item| item.code == "target_changed") + { + return Some("confirm the intended Nix flake target before further work".to_string()); + } + if invalidations + .iter() + .any(|item| item.code == "flake_lock_changed") + { + return Some( + "re-evaluate and rebuild the target before trusting prior evidence".to_string(), + ); + } + if !scope_drift.is_empty() { + return Some("inspect unexpected repository paths before mutation".to_string()); + } + if invalidations + .iter() + .any(|item| item.code == "candidate_system_changed") + { + return Some("compare the active and candidate closures".to_string()); + } + if invalidations + .iter() + .any(|item| item.code == "active_system_changed") + { + return Some("verify the currently active host generation and services".to_string()); + } + if current.candidate_system.is_none() { + return Some("evaluate the configured NixOS system target".to_string()); + } + None +} + +fn resolve_path(repo_root: &Utf8Path, path: &Utf8Path) -> Utf8PathBuf { + if path.is_absolute() { + path.to_path_buf() + } else { + repo_root.join(path) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn observation( + head: &str, + lock: Option<&str>, + current: Option<&str>, + candidate: Option<&str>, + ) -> NixObservation { + NixObservation { + repository: NixRepositoryState { + root: Utf8PathBuf::from("/repo"), + branch: "main".to_string(), + head: head.to_string(), + }, + target: ".#nixosConfigurations.example.config.system.build.toplevel".to_string(), + flake_lock_digest: lock.map(str::to_string), + current_system: current.map(Utf8PathBuf::from), + candidate_system: candidate.map(Utf8PathBuf::from), + changed_paths: Vec::new(), + } + } + + fn spec() -> NixTargetSpec { + NixTargetSpec { + id: "NX-1".to_string(), + target: ".#nixosConfigurations.example.config.system.build.toplevel".to_string(), + flake_lock_path: Utf8PathBuf::from("flake.lock"), + baseline: Some("main".to_string()), + expected_paths: vec!["hosts/example/**".to_string()], + supporting_paths: vec!["flake.lock".to_string()], + constraints: vec!["preserve remote access".to_string()], + checks: vec!["nix flake check".to_string()], + } + } + + #[test] + fn lock_change_invalidates_previous_nix_evidence() { + let mut checkpoint = NixCheckpoint::new( + spec(), + observation( + "old", + Some("old-lock"), + Some("/nix/store/a"), + Some("/nix/store/b"), + ), + ); + checkpoint.evidence.push(NixEvidence { + kind: "build".to_string(), + status: "passed".to_string(), + input_fingerprint: checkpoint.observation.input_fingerprint().unwrap(), + command: Some("nix build".to_string()), + output_ref: None, + }); + + let assessment = assess_nix_resume( + &checkpoint, + observation( + "new", + Some("new-lock"), + Some("/nix/store/a"), + Some("/nix/store/c"), + ), + ) + .unwrap(); + + assert!(assessment + .invalidated_context + .iter() + .any(|item| item.code == "flake_lock_changed")); + assert_eq!(assessment.stale_evidence.len(), 1); + assert_eq!( + assessment.next_probe.as_deref(), + Some("re-evaluate and rebuild the target before trusting prior evidence") + ); + } + + #[test] + fn classifies_unexpected_nix_paths() { + let paths = vec![ + Utf8PathBuf::from("hosts/example/services.nix"), + Utf8PathBuf::from("flake.lock"), + Utf8PathBuf::from("modules/networking/wireguard.nix"), + ]; + + let classified = classify_nix_paths( + &paths, + &["hosts/example/**".to_string()], + &["flake.lock".to_string()], + ); + + assert_eq!(classified[0].relevance, NixPathRelevance::Supporting); + assert_eq!(classified[1].relevance, NixPathRelevance::Expected); + assert_eq!(classified[2].relevance, NixPathRelevance::Unexpected); + } + + #[test] + fn checkpoint_digest_is_deterministic() { + let checkpoint = NixCheckpoint::new( + spec(), + observation( + "head", + Some("lock"), + Some("/nix/store/a"), + Some("/nix/store/b"), + ), + ); + + assert_eq!(checkpoint.digest().unwrap(), checkpoint.digest().unwrap()); + } +} From e56d565b57213dbc5d2467254e986c83e948d972 Mon Sep 17 00:00:00 2001 From: Goudarz Firoozi <63324407+greygoody@users.noreply.github.com> Date: Thu, 23 Jul 2026 20:22:52 +0330 Subject: [PATCH 02/24] feat(nix): add checkpoint and command errors --- crates/codectx-core/src/error.rs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/crates/codectx-core/src/error.rs b/crates/codectx-core/src/error.rs index 80f55ac..2c5223b 100644 --- a/crates/codectx-core/src/error.rs +++ b/crates/codectx-core/src/error.rs @@ -15,6 +15,15 @@ pub enum CodectxError { #[error("focus file does not exist: {0}")] MissingFocusFile(Utf8PathBuf), + #[error("Nix target spec does not exist: {0}")] + MissingNixSpec(Utf8PathBuf), + + #[error("invalid Nix target spec: {0}")] + InvalidNixSpec(String), + + #[error("unsupported Nix checkpoint schema version: {0}")] + UnsupportedNixCheckpointVersion(u32), + #[error("path is outside repo root: {path} (repo root: {repo_root})")] PathOutsideRepo { path: Utf8PathBuf, @@ -27,7 +36,10 @@ pub enum CodectxError { #[error("git command failed: git {args}: {stderr}")] GitCommandFailed { args: String, stderr: String }, - #[error("git output was not valid UTF-8")] + #[error("nix command failed: nix {args}: {stderr}")] + NixCommandFailed { args: String, stderr: String }, + + #[error("command output was not valid UTF-8")] GitOutputNonUtf8(#[from] std::string::FromUtf8Error), #[error("invalid event JSONL at {path}:{line}")] From 71c6d164936ba493f7b17df211d05d5f72c9ace8 Mon Sep 17 00:00:00 2001 From: Goudarz Firoozi <63324407+greygoody@users.noreply.github.com> Date: Thu, 23 Jul 2026 20:23:09 +0330 Subject: [PATCH 03/24] feat(nix): expose NixOS checkpoint API --- crates/codectx-core/src/lib.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/crates/codectx-core/src/lib.rs b/crates/codectx-core/src/lib.rs index ce00106..83d759c 100644 --- a/crates/codectx-core/src/lib.rs +++ b/crates/codectx-core/src/lib.rs @@ -4,6 +4,7 @@ pub mod event_store; pub mod git; pub mod hash; pub mod lens; +pub mod nix; pub mod packet; pub mod path_rule; pub mod repo; @@ -31,6 +32,13 @@ pub use event_store::EventStore; pub use git::{CommandGitProbe, GitProbe, GitState}; pub use hash::{sha256_bytes, sha256_file}; pub use lens::{enter_task, read_context, IncludedFile, LensStatus, ProofCommand, TaskLens}; +pub use nix::{ + assess_nix_resume, classify_nix_paths, render_nix_observation, render_nix_resume, + CommandNixProbe, NixChangedPath, NixCheckpoint, NixCheckpointNotes, NixContextBuilder, + NixEvidence, NixInvalidation, NixObservation, NixPathRelevance, NixProbe, + NixRepositoryState, NixResumeAssessment, NixStaleEvidence, NixTargetSpec, + DEFAULT_NIX_CHECKPOINT_PATH, DEFAULT_NIX_SPEC_PATH, NIX_CHECKPOINT_SCHEMA_VERSION, +}; pub use packet::{ classify_changes, render_context_hud, render_review, write_context_packet, AttentionItem, AttentionLevel, ChangeContext, ChangeRelevance, ClassifiedChange, ContextAction, ContextPacket, From 35a144687bb7a5fa22e241641a2738c81cd30e0b Mon Sep 17 00:00:00 2001 From: Goudarz Firoozi <63324407+greygoody@users.noreply.github.com> Date: Thu, 23 Jul 2026 20:23:26 +0330 Subject: [PATCH 04/24] refactor(cli): make NixOS workflow the primary product surface --- crates/codectx-cli/Cargo.toml | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/crates/codectx-cli/Cargo.toml b/crates/codectx-cli/Cargo.toml index f7d2b30..a618786 100644 --- a/crates/codectx-cli/Cargo.toml +++ b/crates/codectx-cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "codectx-cli" -version = "0.1.0" +version = "0.2.0" edition.workspace = true license.workspace = true repository.workspace = true @@ -9,10 +9,6 @@ repository.workspace = true name = "codectx" path = "src/context_main.rs" -[[bin]] -name = "codectx-legacy" -path = "src/main.rs" - [dependencies] camino.workspace = true anyhow.workspace = true From 9725e98f3059b5c1d9facc8fa22c7d10b5f7cefb Mon Sep 17 00:00:00 2001 From: Goudarz Firoozi <63324407+greygoody@users.noreply.github.com> Date: Thu, 23 Jul 2026 20:23:55 +0330 Subject: [PATCH 05/24] feat(cli): add nix inspect checkpoint and resume commands --- crates/codectx-cli/src/context_main.rs | 292 ++++++++----------------- 1 file changed, 96 insertions(+), 196 deletions(-) diff --git a/crates/codectx-cli/src/context_main.rs b/crates/codectx-cli/src/context_main.rs index bfed6c3..0e40c27 100644 --- a/crates/codectx-cli/src/context_main.rs +++ b/crates/codectx-cli/src/context_main.rs @@ -1,181 +1,124 @@ -use anyhow::{anyhow, bail, Context, Result}; +use anyhow::{Context, Result}; use camino::{Utf8Path, Utf8PathBuf}; +use clap::{Parser, Subcommand}; use codectx_core::{ - find_repo_root, render_context_hud, render_review, write_context_packet, CodectxError, - ContextPacketBuilder, + assess_nix_resume, find_repo_root, render_nix_observation, render_nix_resume, NixCheckpoint, + NixContextBuilder, NixTargetSpec, DEFAULT_NIX_CHECKPOINT_PATH, DEFAULT_NIX_SPEC_PATH, }; -use std::{env, path::PathBuf, process::Command}; - -const LEGACY_COMMANDS: &[&str] = &[ - "enter", "read", "context", "snapshot", "surface", "net-move", "ok", "prove", "done", -]; +use std::{env, path::PathBuf}; + +#[derive(Debug, Parser)] +#[command( + name = "codectx", + version, + about = "Verify and resume bounded NixOS configuration changes" +)] +struct Cli { + #[command(subcommand)] + command: Command, +} -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum Mode { - Context, - Review, - Handoff, +#[derive(Debug, Subcommand)] +enum Command { + /// Inspect, checkpoint, or resume a NixOS configuration change. + Nix { + #[command(subcommand)] + command: NixCommand, + }, } -#[derive(Debug)] -struct ParsedArgs { - mode: Mode, - json: bool, - focus: Option, - baseline: Option, - runseal_closures: Vec, - output: Option, +#[derive(Debug, Subcommand)] +enum NixCommand { + /// Observe the configured NixOS target without mutating or activating it. + Inspect { + /// Nix target specification. Defaults to .codectx/nix.toml. + #[arg(long, default_value = DEFAULT_NIX_SPEC_PATH)] + spec: PathBuf, + /// Print machine-readable JSON. + #[arg(long)] + json: bool, + }, + /// Write a deterministic checkpoint for the current NixOS target state. + Checkpoint { + /// Nix target specification. Defaults to .codectx/nix.toml. + #[arg(long, default_value = DEFAULT_NIX_SPEC_PATH)] + spec: PathBuf, + /// Checkpoint destination. + #[arg(long, default_value = DEFAULT_NIX_CHECKPOINT_PATH)] + output: PathBuf, + /// Print the checkpoint JSON after writing it. + #[arg(long)] + json: bool, + }, + /// Compare the current NixOS target state with a previous checkpoint. + Resume { + /// Checkpoint to assess. + #[arg(long, default_value = DEFAULT_NIX_CHECKPOINT_PATH)] + checkpoint: PathBuf, + /// Print machine-readable JSON. + #[arg(long)] + json: bool, + }, } fn main() -> Result<()> { - let args: Vec = env::args().skip(1).collect(); - - if let Some(legacy_args) = legacy_args(&args) { - return forward_legacy(&legacy_args); - } - - if args.iter().any(|arg| arg == "--help" || arg == "-h") { - print_help(); - return Ok(()); - } - - let parsed = parse_args(args)?; - let start = env::current_dir()?; - if let Some(focus) = &parsed.focus { - let repo_root = find_repo_root(&start)?; - let resolved = resolve_path(&repo_root, focus); - if !resolved.exists() { - return Err(CodectxError::MissingFocusFile(focus.clone()).into()); - } - } - - let mut builder = ContextPacketBuilder::new(); - if let Some(baseline) = parsed.baseline { - builder = builder.baseline(baseline); - } - if let Some(focus) = parsed.focus { - builder = builder.focus_path(focus); - } - for closure in parsed.runseal_closures { - builder = builder.runseal_closure(closure); + let cli = Cli::parse(); + match cli.command { + Command::Nix { command } => run_nix(command), } +} - let packet = builder.build_current(start)?; - match parsed.mode { - Mode::Context => { - if parsed.json { - print!("{}", packet.to_pretty_json()?); +fn run_nix(command: NixCommand) -> Result<()> { + let cwd = env::current_dir().context("resolve current directory")?; + let repo_root = find_repo_root(&cwd)?; + let builder = NixContextBuilder::default(); + + match command { + NixCommand::Inspect { spec, json } => { + let spec_path = utf8_path(spec)?; + let spec = NixTargetSpec::load(&repo_root, &spec_path)?; + let observation = builder.inspect(&cwd, &spec)?; + if json { + println!("{}", serde_json::to_string_pretty(&observation)?); } else { - print!("{}", render_context_hud(&packet)); + print!("{}", render_nix_observation(&spec, &observation)); } } - Mode::Review => { - if parsed.json { - print!("{}", packet.to_pretty_json()?); + NixCommand::Checkpoint { spec, output, json } => { + let spec_path = utf8_path(spec)?; + let output = resolve_path(&repo_root, &utf8_path(output)?); + let spec = NixTargetSpec::load(&repo_root, &spec_path)?; + let observation = builder.inspect(&cwd, &spec)?; + let checkpoint = NixCheckpoint::new(spec, observation); + checkpoint.write(&output)?; + + if json { + print!("{}", checkpoint.to_pretty_json()?); } else { - print!("{}", render_review(&packet)); + println!("checkpoint written: {output}"); + println!("checkpoint digest: {}", checkpoint.digest()?); } } - Mode::Handoff => { - let output = parsed - .output - .unwrap_or_else(|| Utf8PathBuf::from(".codectx/handoffs/latest.json")); - let output = resolve_path(&packet.repository.root, &output); - write_context_packet(&output, &packet)?; - if parsed.json { - print!("{}", packet.to_pretty_json()?); + NixCommand::Resume { checkpoint, json } => { + let checkpoint_path = resolve_path(&repo_root, &utf8_path(checkpoint)?); + let checkpoint = NixCheckpoint::load(&checkpoint_path)?; + let current = builder.inspect(&cwd, &checkpoint.spec)?; + let assessment = assess_nix_resume(&checkpoint, current)?; + + if json { + println!("{}", serde_json::to_string_pretty(&assessment)?); } else { - println!("handoff written: {output}"); - print!("{}", render_context_hud(&packet)); - } - } - } - - Ok(()) -} - -fn legacy_args(args: &[String]) -> Option> { - let first = args.first()?; - if first == "check" { - let mut mapped = args.to_vec(); - mapped[0] = "ok".to_string(); - return Some(mapped); - } - LEGACY_COMMANDS - .contains(&first.as_str()) - .then(|| args.to_vec()) -} - -fn parse_args(args: Vec) -> Result { - let mut mode = Mode::Context; - let mut mode_was_set = false; - let mut json = false; - let mut focus = None; - let mut baseline = None; - let mut runseal_closures = Vec::new(); - let mut output = None; - let mut index = 0usize; - - while index < args.len() { - match args[index].as_str() { - "packet" => set_mode(&mut mode, &mut mode_was_set, Mode::Context)?, - "review" => set_mode(&mut mode, &mut mode_was_set, Mode::Review)?, - "handoff" => set_mode(&mut mode, &mut mode_was_set, Mode::Handoff)?, - "--json" => json = true, - "--focus" => { - index += 1; - focus = Some(next_path(&args, index, "--focus")?); - } - "--baseline" => { - index += 1; - baseline = Some(next_value(&args, index, "--baseline")?); + print!("{}", render_nix_resume(&assessment)); } - "--runseal-closure" => { - index += 1; - runseal_closures.push(next_path(&args, index, "--runseal-closure")?); - } - "--output" => { - index += 1; - output = Some(next_path(&args, index, "--output")?); - } - unknown => bail!("unknown Codectx context option: {unknown}"), } - index += 1; } - if output.is_some() && mode != Mode::Handoff { - bail!("--output is only valid with `codectx handoff`"); - } - - Ok(ParsedArgs { - mode, - json, - focus, - baseline, - runseal_closures, - output, - }) -} - -fn set_mode(mode: &mut Mode, was_set: &mut bool, next: Mode) -> Result<()> { - if *was_set && *mode != next { - bail!("choose only one of `packet`, `review`, or `handoff`"); - } - *mode = next; - *was_set = true; Ok(()) } -fn next_value(args: &[String], index: usize, option: &str) -> Result { - args.get(index) - .cloned() - .ok_or_else(|| anyhow!("{option} requires a value")) -} - -fn next_path(args: &[String], index: usize, option: &str) -> Result { - let value = next_value(args, index, option)?; - Utf8PathBuf::from_path_buf(PathBuf::from(value)) - .map_err(|path| anyhow!("{option} path is not valid UTF-8: {}", path.display())) +fn utf8_path(path: PathBuf) -> Result { + Utf8PathBuf::from_path_buf(path) + .map_err(|path| anyhow::anyhow!("path is not valid UTF-8: {}", path.display())) } fn resolve_path(repo_root: &Utf8Path, path: &Utf8Path) -> Utf8PathBuf { @@ -185,46 +128,3 @@ fn resolve_path(repo_root: &Utf8Path, path: &Utf8Path) -> Utf8PathBuf { repo_root.join(path) } } - -fn forward_legacy(args: &[String]) -> Result<()> { - let current = env::current_exe().context("resolve current codectx executable")?; - let mut legacy = current.clone(); - legacy.set_file_name(format!("codectx-legacy{}", env::consts::EXE_SUFFIX)); - if !legacy.exists() { - bail!( - "legacy compatibility binary was not found at {}; rebuild or install both codectx binaries", - legacy.display() - ); - } - - let status = Command::new(&legacy) - .args(args) - .status() - .with_context(|| format!("run legacy compatibility binary {}", legacy.display()))?; - std::process::exit(status.code().unwrap_or(1)); -} - -fn print_help() { - println!( - "codectx - compile repository signals into focused human and agent context\n\ -\nUSAGE:\n\ - codectx [--json] [--focus PATH] [--baseline REV] [--runseal-closure PATH]...\n\ - codectx review [OPTIONS]\n\ - codectx handoff [--output PATH] [OPTIONS]\n\ -\nPRIMARY COMMANDS:\n\ - packet Render the default context packet (also the plain-command behavior)\n\ - review Render changed paths, constraints, commands, and attention items\n\ - handoff Write a deterministic handoff packet; defaults to .codectx/handoffs/latest.json\n\ -\nOPTIONS:\n\ - --json Emit the deterministic machine-readable packet\n\ - --focus PATH Load an explicit focus TOML file; a missing explicit path is an error\n\ - --baseline REV Compare repository changes with a Git revision\n\ - --runseal-closure PATH Import a Runseal closure record as an external signal\n\ - --output PATH Handoff output path\n\ -\nLEGACY COMPATIBILITY COMMANDS:\n\ - ok Check whether the active context is safe to stop\n\ - check Compatibility alias mapped to legacy `ok`\n\ - enter, read, context, snapshot, surface, net-move, prove, done\n\ -\nCodectx renders context. It does not resolve Runseal closures or promote external claims into proof." - ); -} From 343911a9d0ff73b2394452eda629a2e018b5b96d Mon Sep 17 00:00:00 2001 From: Goudarz Firoozi <63324407+greygoody@users.noreply.github.com> Date: Thu, 23 Jul 2026 20:24:08 +0330 Subject: [PATCH 06/24] docs(nix): add public target specification example --- .codectx/nix.example.toml | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 .codectx/nix.example.toml diff --git a/.codectx/nix.example.toml b/.codectx/nix.example.toml new file mode 100644 index 0000000..225d0b3 --- /dev/null +++ b/.codectx/nix.example.toml @@ -0,0 +1,25 @@ +id = "NX-0001" +target = ".#nixosConfigurations.example.config.system.build.toplevel" +flake_lock_path = "flake.lock" +baseline = "main" + +expected_paths = [ + "hosts/example/**", +] + +supporting_paths = [ + "flake.nix", + "flake.lock", + "modules/**", + "tests/**", +] + +constraints = [ + "preserve remote access", + "do not activate a system that has not been built and reviewed", +] + +checks = [ + "nix flake check", + "nix build .#nixosConfigurations.example.config.system.build.toplevel", +] From fc950e75ca240d4210bcd28f2ee35e1aa08c468c Mon Sep 17 00:00:00 2001 From: Goudarz Firoozi <63324407+greygoody@users.noreply.github.com> Date: Thu, 23 Jul 2026 20:24:17 +0330 Subject: [PATCH 07/24] chore: ignore local Nix target and checkpoints --- .gitignore | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 0bdfde3..e05717d 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,8 @@ target # RustRover / JetBrains local config #.idea/ -# Local Codectx projections +# Local Codectx state /.codectx/focus.toml /.codectx/handoffs/ +/.codectx/nix.toml +/.codectx/checkpoints/ From 1b61cf8b88aaa4b61755f35db32437fb7bbe59d3 Mon Sep 17 00:00:00 2001 From: Goudarz Firoozi <63324407+greygoody@users.noreply.github.com> Date: Thu, 23 Jul 2026 20:24:54 +0330 Subject: [PATCH 08/24] docs: reposition Codectx as a NixOS checkpoint tool --- README.md | 349 ++++++++++++++++++++---------------------------------- 1 file changed, 126 insertions(+), 223 deletions(-) diff --git a/README.md b/README.md index 353a53d..7efe508 100644 --- a/README.md +++ b/README.md @@ -1,304 +1,207 @@ # Codectx -**An always-present contextual HUD for humans, virtual agents, and coding tools.** +**Deterministic checkpoints and stale-context detection for bounded NixOS changes.** -Codectx projects the minimum operational information a worker needs to perceive its current environment, focus, constraints, risks, and available moves without stopping work to reconstruct context through a separate interface. +Codectx helps a human or coding agent answer a narrow operational question: -The current repository implements Codectx's first proven lens: deterministic repository context. It observes a Git repository, combines explicit focus with project commands and imported external signals, and emits one bounded projection for human HUD, machine consumption, review, and handoff. +> Does the NixOS context from the previous work session still apply to the repository and target I am looking at now? -Git remains repository authority. Codectx is a projection and attention system, not a task database, proof authority, workflow orchestrator, or source of domain truth. +It binds a declared change to concrete identities: -The canonical HUD direction and migration boundaries are defined in [`docs/codectx-hud-v0/README.md`](docs/codectx-hud-v0/README.md). +- Git repository and HEAD; +- the selected flake target; +- the `flake.lock` content digest; +- changed-path scope; +- the active `/run/current-system` closure when running on NixOS; +- the currently evaluated candidate system store path. -## Product direction +Codectx then writes a deterministic checkpoint and compares later repository state against it. When inputs change, it reports which prior assumptions and evidence are stale instead of allowing an agent to repeat "the build passed" after the configuration that produced that build has changed. + +Codectx does **not** deploy systems, authorize effects, replace the Nix evaluator, own host truth, or infer that a configuration is safe merely because evaluation succeeded. + +## Current vertical slice + +The public CLI has one product surface: ```text -World, runtime, repository, Orb, and domain sources - ↓ - Codectx lenses - ↓ - composed projection - ↓ - ambient HUD / machine view / review / handoff +codectx nix inspect +codectx nix checkpoint +codectx nix resume ``` -Codectx should eventually remain continuously available through a host runtime or device such as Orb. The worker should normally acquire context by perception rather than by opening another application and navigating to the relevant state. +Implemented now: + +- load one explicit Nix target specification; +- observe Git state against an optional baseline; +- classify changed paths as expected, supporting, or unexpected; +- hash `flake.lock`; +- evaluate one NixOS toplevel target with `nix eval --raw`; +- observe `/run/current-system` when available; +- write deterministic JSON checkpoints; +- detect target, Git, lockfile, candidate-system, and active-system changes; +- mark evidence recorded against another input fingerprint as stale; +- recommend the next inspection step. -The current CLI remains valid. It is the first explicit shell and proof surface for the projection model, not obsolete scaffolding awaiting ceremonial deletion. +Not implemented yet: -## Implemented repository lens +- building or activating a NixOS system; +- closure diffs; +- VM or integration-test execution; +- recording command evidence from the CLI; +- deployment-tool adapters; +- remote host inspection; +- Orb execution grants or MCP transport. -- human-readable HUD through plain `codectx`; -- deterministic machine packet through `codectx --json`; -- Git root, branch, HEAD, dirty files, and baseline-relative changes; -- explicit focus from `.codectx/focus.toml` or `--focus PATH`; -- expected, supporting, unexpected, and unclassified path handling; -- public `justfile` command discovery with definition hashes; -- `codectx review` reviewer output; -- `codectx handoff` durable JSON handoff; -- Runseal records imported as external claims with unknown freshness; -- the earlier session runtime preserved as `codectx-legacy`. +Those are intentionally deferred until the checkpoint and invalidation behavior proves useful across repeated real changes. -Codectx does not currently implement an ambient compositor runtime, continuous subscriptions, lens registration, Orb integration, closure resolution, state-bound evidence execution, generic risk assessment, orchestration, repository indexing, a daemon, or a database. +## Quick start -## Quickstart +Requirements: + +- Rust toolchain; +- Git; +- Nix with flakes enabled; +- a flake exposing a NixOS system derivation. + +Build Codectx: ```bash -git clone https://github.com/greygoody/codectx.git -cd codectx -cargo build -p codectx-cli --bins -mkdir -p .codectx -cp .codectx/focus.example.toml .codectx/focus.toml -./target/debug/codectx +cargo build -p codectx-cli --bin codectx ``` -Primary surfaces: +Create a local target specification: ```bash -./target/debug/codectx -./target/debug/codectx --json -./target/debug/codectx review -./target/debug/codectx handoff +mkdir -p .codectx +cp .codectx/nix.example.toml .codectx/nix.toml ``` -The default handoff path is `.codectx/handoffs/latest.json`. The repository ignores `.codectx/focus.toml` and `.codectx/handoffs/`, so normal local use does not manufacture new repository noise. - -## Focus declaration +Edit the target to match your repository: ```toml -id = "CX-0001" -title = "Add deterministic context packet output" -summary = "Expose focused repository state to implementers and reviewers." +id = "NX-0001" +target = ".#nixosConfigurations.example.config.system.build.toplevel" +flake_lock_path = "flake.lock" +baseline = "main" expected_paths = [ - "crates/codectx-core/**", - "crates/codectx-cli/**", + "hosts/example/**", ] supporting_paths = [ - "README.md", - "docs/**", - ".github/**", + "flake.nix", + "flake.lock", + "modules/**", + "tests/**", ] constraints = [ - "preserve Git as repository authority", - "do not promote external claims into proof", - "derive human and JSON output from one packet", + "preserve remote access", + "do not activate a system that has not been built and reviewed", ] -``` -The default `.codectx/focus.toml` is optional. When it is absent, Codectx reports `focus_missing` and leaves changed paths unclassified. - -An explicitly requested path is a different contract. This fails instead of silently degrading to no focus: - -```bash -codectx --focus missing-focus.toml +checks = [ + "nix flake check", +] ``` -## Fixture-backed product walkthrough - -Run the committed demonstration: +Inspect the current state: ```bash -bash scripts/demo.sh -``` - -The script copies `fixtures/context-demo/repository` into a temporary Git repository, commits its baseline, and applies one expected source change, one supporting documentation change, and one unexpected operations change. - -It verifies the HUD, review, deterministic JSON, public command discovery, hidden-command filtering, external-claim boundary, attention items, and byte equality between `codectx --json` and the default handoff. - -### Human HUD example - -```text -CODECTX · context - -Location - demo · - -Focus - DEMO-0001 · Add request tracing - Keep request-tracing changes focused while preserving the repository command surface. - -Changes - expected 1 · supporting 1 · unexpected 1 · unclassified 0 - -Attention - - external_obligation_open: Runseal closure RS-DEMO externally reports status open. - - unexpected_changes: 1 changed path(s) do not match the declared focus. - -Next - Inspect unexpected changes - Changed paths fall outside the declared expected and supporting paths. - run: git diff --stat +./target/debug/codectx nix inspect ``` -### Review example - -```text -Changed paths - - README.md [Supporting] - - ops/emergency.toml [Unexpected] - - src/lib.rs [Expected] - -Constraints - - keep the public API stable - - keep Git provenance visible - - treat imported obligation status as an external claim - -Available commands - - just check - - just format -``` - -### JSON excerpt - -```json -{ - "schema_version": 1, - "repository": { - "root": "", - "branch": "demo", - "head": "" - }, - "focus": { - "id": "DEMO-0001", - "title": "Add request tracing" - }, - "changes": { - "changed_files": [ - { "path": "README.md", "relevance": "supporting" }, - { "path": "ops/emergency.toml", "relevance": "unexpected" }, - { "path": "src/lib.rs", "relevance": "expected" } - ] - }, - "runnables": [ - { "id": "check", "command": "just check", "visibility": "public" }, - { "id": "format", "command": "just format", "visibility": "public" } - ], - "external_signals": [ - { - "id": "RS-DEMO", - "epistemic_status": "external_claim", - "freshness": "unknown" - } - ] -} -``` - -## Projection authority - -Runseal owns persistent obligations, typed conditions, evidence semantics, resolution, and regression. +Write a checkpoint: ```bash -./target/debug/codectx \ - --runseal-closure ../runseal/closures/RS-0001.toml +./target/debug/codectx nix checkpoint ``` -Imported records remain external claims with unknown freshness. Codectx observes the record; it does not independently validate the obligation or promote its status into Codectx-owned truth. +After more work, assess whether the old context is still valid: -```text -Git / focus / justfile / external record - ↓ - ContextPacket v1 - ↓ - HUD / JSON / review / handoff +```bash +./target/debug/codectx nix resume ``` -In the broader direction, `ContextPacket v1` becomes the compatibility contract for the repository lens. It is not silently redefined into a universal HUD schema. +Machine-readable output is available with `--json` on all three commands. -## Context state classes - -Future Codectx projections should preserve three origins: +## Example resume result ```text -derived - computed from observed sources - -managed - supplied or required by a runtime, device, authority, or policy source +CODECTX · nixos resume -explicit - changed by a deliberate user or bearer move +Checkpoint: +Invalidated context: + - flake_lock_changed: flake.lock changed; prior evaluation, build, test, and closure-diff evidence must be repeated + - candidate_system_changed: the evaluated NixOS system store path changed +Scope drift: + - modules/networking/wireguard.nix +Next probe: re-evaluate and rebuild the target before trusting prior evidence ``` -Current focus files and CLI options are explicit input. Git observations and change relevance are derived. Imported external records remain managed external claims only when their source and authority are preserved. - -## v0.2 interface decisions +The command does not claim that the change is unsafe. It reports that prior evidence is no longer applicable and identifies the next bounded probe. -### Legacy commands +## Contract -The previous bounded-session runtime is explicitly available as: +A checkpoint contains: -```bash -./target/debug/codectx-legacy --help +```text +NixCheckpoint { + schema_version + spec + observation { + repository + target + flake_lock_digest? + current_system? + candidate_system? + changed_paths[] + } + notes + evidence[] +} ``` -For v0.2 compatibility, implemented historical commands such as `codectx enter`, `codectx ok`, and `codectx prove` are forwarded to the sibling `codectx-legacy` binary when both binaries are installed together. `codectx check` is an explicit alias mapped to legacy `ok`. Commands not implemented by the preserved legacy binary, including `events`, are not advertised. This is an intentional compatibility policy, not shared semantics between the two runtimes. +The checkpoint body excludes timestamps, so identical state produces an identical digest. Runtime systems may wrap it with timestamps, identities, signatures, or storage metadata without changing the deterministic checkpoint contract. -Legacy manually recorded proof remains an attestation, not repository-state-bound evidence. +## Authority boundaries -### Handoff output +Codectx owns: -The primary managed destination is `.codectx/handoffs/latest.json`, and the walkthrough proves that path. v0.2 also retains `codectx handoff --output PATH` for explicit integrations. Arbitrary output is not used by the normal repository workflow. +- Nix workflow checkpoint schemas; +- source and input fingerprints; +- context comparison and invalidation; +- changed-path scope classification; +- restart and handoff projections. -## Architecture +Codectx does not own: -```text -crates/codectx-core - repository observation, command discovery, packet construction, renderers +- Git repository truth; +- Nix evaluation or store truth; +- NixOS activation state beyond what it observes; +- deployment authorization; +- service-health truth; +- proof merely because a command or external system reported success. -crates/codectx-cli - explicit HUD shell, machine packet, review, handoff, compatibility binary +## Private operational use -crates/codectx-protocol - historical session/event protocol used by compatibility code -``` +`.codectx/nix.toml` and `.codectx/checkpoints/` are ignored by Git. Real host names, infrastructure paths, service constraints, deployment evidence, and operational checkpoints should stay in private repositories or runtime storage. -Target direction, not implemented in Step 1: +The public repository contains only synthetic examples. Private evaluation against real machines must not be copied into fixtures, issues, logs, screenshots, or committed checkpoints. -```text -Codectx lenses - bounded context providers +## Migration status -Codectx compositor - attention, priority, visibility, and projection composition +The repository still contains historical repository-context and bounded-session modules because they hold tested Git, path, hashing, and validity primitives. They are no longer exposed by the primary CLI and are migration material, not the product direction. -Codectx surfaces - ambient HUD, CLI, machine API, review, and handoff renderers - -Host runtime or device - subscriptions, lifecycle, delivery, persistence, and authority integration -``` - -See [`docs/context-packet-v1.md`](docs/context-packet-v1.md), [`docs/implementation-status.md`](docs/implementation-status.md), and [`docs/codectx-hud-v0/README.md`](docs/codectx-hud-v0/README.md). +They should be removed or moved to a historical tag after the Nix checkpoint slice has replacement tests and has been exercised across repeated real NixOS changes. Preserving them indefinitely would merely turn archaeological layers into architecture. ## Proof -```bash -just check -``` - -Equivalent witnesses: - ```bash cargo fmt --all --check cargo clippy --workspace --all-targets -- -D warnings cargo test --workspace cargo run -p codectx-cli --bin codectx -- --help -bash scripts/demo.sh ``` -## Design rules - -1. Context is a projection, not domain authority. -2. Codectx reduces context acquisition from an interaction into perception. -3. Observed, derived, managed, explicit, external claim, and unknown remain distinguishable. -4. Git stays visible and authoritative for repository state. -5. Missing data is not fabricated. -6. Human and machine views derive from bounded deterministic projections. -7. Attention is prioritized; healthy state should collapse rather than occupy the entire field of view. -8. Codectx suggests or exposes moves but does not award completion or manufacture authorization. -9. External systems retain ownership of their semantics. -10. Writes remain within explicit Codectx artifact or move boundaries. -11. The current repository lens and ContextPacket v1 remain compatibility surfaces until versioned successors are proven. +The next proof milestone is not another architecture document. It is ten real, private NixOS changes with measurements for checkpoint overhead, resume latency, stale-evidence detections, scope-drift detections, false warnings, and whether Codectx changed the next action. From ffe6378ca140c5d09eba35fc76607cdf5b8708ed Mon Sep 17 00:00:00 2001 From: Goudarz Firoozi <63324407+greygoody@users.noreply.github.com> Date: Thu, 23 Jul 2026 20:25:30 +0330 Subject: [PATCH 09/24] test(cli): remove legacy forwarding contract --- .../tests/legacy_forwarding_contract.rs | 55 ------------------- 1 file changed, 55 deletions(-) delete mode 100644 crates/codectx-cli/tests/legacy_forwarding_contract.rs diff --git a/crates/codectx-cli/tests/legacy_forwarding_contract.rs b/crates/codectx-cli/tests/legacy_forwarding_contract.rs deleted file mode 100644 index 255ab02..0000000 --- a/crates/codectx-cli/tests/legacy_forwarding_contract.rs +++ /dev/null @@ -1,55 +0,0 @@ -use std::process::{Command, Output}; - -#[test] -fn legacy_subcommand_help_is_forwarded_before_global_help() { - let output = codectx(["ok", "--help"]); - assert_success(&output); - let stdout = String::from_utf8(output.stdout).unwrap(); - - assert!( - stdout.contains("Check whether the active context is safe to stop"), - "stdout:\n{stdout}" - ); - assert!( - !stdout.contains("compile repository signals into focused human and agent context"), - "new CLI help intercepted legacy help:\n{stdout}" - ); -} - -#[test] -fn check_alias_maps_to_legacy_ok() { - let output = codectx(["check", "--help"]); - assert_success(&output); - let stdout = String::from_utf8(output.stdout).unwrap(); - - assert!( - stdout.contains("Check whether the active context is safe to stop"), - "stdout:\n{stdout}" - ); -} - -#[test] -fn root_help_does_not_advertise_unimplemented_events_command() { - let output = codectx(["--help"]); - assert_success(&output); - let stdout = String::from_utf8(output.stdout).unwrap(); - - assert!(!stdout.contains("events"), "stdout:\n{stdout}"); - assert!(stdout.contains("check Compatibility alias mapped to legacy `ok`")); -} - -fn codectx(args: [&str; N]) -> Output { - Command::new(env!("CARGO_BIN_EXE_codectx")) - .args(args) - .output() - .expect("run codectx") -} - -fn assert_success(output: &Output) { - assert!( - output.status.success(), - "command failed\nstdout:\n{}\nstderr:\n{}", - String::from_utf8_lossy(&output.stdout), - String::from_utf8_lossy(&output.stderr) - ); -} From b42f0da6a63719ea262985668b714aad4c46edab Mon Sep 17 00:00:00 2001 From: Goudarz Firoozi <63324407+greygoody@users.noreply.github.com> Date: Thu, 23 Jul 2026 20:25:35 +0330 Subject: [PATCH 10/24] test(cli): remove repository context packet contract --- .../tests/context_packet_contract.rs | 198 ------------------ 1 file changed, 198 deletions(-) delete mode 100644 crates/codectx-cli/tests/context_packet_contract.rs diff --git a/crates/codectx-cli/tests/context_packet_contract.rs b/crates/codectx-cli/tests/context_packet_contract.rs deleted file mode 100644 index 947a3bc..0000000 --- a/crates/codectx-cli/tests/context_packet_contract.rs +++ /dev/null @@ -1,198 +0,0 @@ -use serde_json::Value; -use std::{ - env, fs, - path::{Path, PathBuf}, - process::{Command, Output}, - time::{SystemTime, UNIX_EPOCH}, -}; - -#[test] -fn plain_json_review_and_handoff_share_one_packet() { - let repo = FixtureRepo::new(); - - let json = codectx( - &repo, - ["--json", "--runseal-closure", "closures/RS-0001.toml"], - ) - .success_json(); - assert_eq!(json["schema_version"], 1); - assert_eq!(json["focus"]["id"], "CX-0001"); - assert_eq!(json["external_signals"][0]["kind"], "runseal_closure"); - assert_eq!( - json["external_signals"][0]["epistemic_status"], - "external_claim" - ); - assert_eq!(json["external_signals"][0]["freshness"], "unknown"); - - let hud = codectx(&repo, ["--runseal-closure", "closures/RS-0001.toml"]).success_stdout(); - for heading in ["CODECTX · context", "Focus", "Changes", "Attention", "Next"] { - assert!(hud.contains(heading), "missing {heading:?}\n{hud}"); - } - - let review = codectx(&repo, ["review"]).success_stdout(); - assert!(review.contains("src/lib.rs [Expected]"), "{review}"); - assert!(review.contains("Cargo.toml [Unexpected]"), "{review}"); - assert!(review.contains("just proof"), "{review}"); - - codectx( - &repo, - [ - "handoff", - "--output", - ".codectx/handoffs/test.json", - "--runseal-closure", - "closures/RS-0001.toml", - ], - ) - .success_stdout(); - let stored: Value = serde_json::from_str( - &fs::read_to_string(repo.path().join(".codectx/handoffs/test.json")).unwrap(), - ) - .unwrap(); - assert_eq!(stored, json); -} - -#[test] -fn explicit_missing_focus_path_is_an_error() { - let repo = FixtureRepo::new(); - - let stderr = codectx(&repo, ["--focus", "missing-focus.toml"]).failure_stderr(); - - assert!( - stderr.contains("focus file does not exist: missing-focus.toml"), - "stderr:\n{stderr}" - ); -} - -fn codectx(repo: &FixtureRepo, args: I) -> CommandOutput -where - I: IntoIterator, - S: AsRef, -{ - CommandOutput { - output: Command::new(env!("CARGO_BIN_EXE_codectx")) - .args(args) - .current_dir(repo.path()) - .output() - .expect("run codectx"), - } -} - -struct CommandOutput { - output: Output, -} - -impl CommandOutput { - fn success(self) -> Output { - assert!( - self.output.status.success(), - "command failed\nstdout:\n{}\nstderr:\n{}", - String::from_utf8_lossy(&self.output.stdout), - String::from_utf8_lossy(&self.output.stderr) - ); - self.output - } - - fn success_stdout(self) -> String { - String::from_utf8(self.success().stdout).unwrap() - } - - fn success_json(self) -> Value { - serde_json::from_str(&self.success_stdout()).unwrap() - } - - fn failure(self) -> Output { - assert!( - !self.output.status.success(), - "command unexpectedly succeeded\nstdout:\n{}\nstderr:\n{}", - String::from_utf8_lossy(&self.output.stdout), - String::from_utf8_lossy(&self.output.stderr) - ); - self.output - } - - fn failure_stderr(self) -> String { - String::from_utf8(self.failure().stderr).unwrap() - } -} - -struct FixtureRepo { - path: PathBuf, -} - -impl FixtureRepo { - fn new() -> Self { - let nanos = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_nanos(); - let path = env::temp_dir().join(format!( - "codectx-public-context-{}-{nanos}", - std::process::id() - )); - let fixture = Self { path }; - fixture.init(); - fixture - } - - fn path(&self) -> &Path { - &self.path - } - - fn init(&self) { - fs::create_dir_all(self.path.join("src")).unwrap(); - fs::create_dir_all(self.path.join(".codectx")).unwrap(); - fs::create_dir_all(self.path.join("closures")).unwrap(); - fs::write(self.path.join("src/lib.rs"), "pub fn initial() {}\n").unwrap(); - fs::write(self.path.join("README.md"), "fixture\n").unwrap(); - fs::write( - self.path.join("justfile"), - "proof:\n cargo test --workspace\n", - ) - .unwrap(); - fs::write( - self.path.join(".codectx/focus.toml"), - "id = \"CX-0001\"\ntitle = \"Compile repository context\"\nexpected_paths = [\"src/**\"]\nsupporting_paths = [\"README.md\"]\nconstraints = [\"do not invent authority\"]\n", - ) - .unwrap(); - fs::write( - self.path.join("closures/RS-0001.toml"), - "id = \"RS-0001\"\ntitle = \"Typed shared-condition graph kernel\"\nstatus = \"resolved\"\nrequirements = [\"typed connections\"]\nproof_commands = [\"just proof\"]\n", - ) - .unwrap(); - git(&self.path, &["init", "-q"]); - git( - &self.path, - &["config", "user.email", "codectx@example.invalid"], - ); - git(&self.path, &["config", "user.name", "Codectx Test"]); - git(&self.path, &["add", "."]); - git(&self.path, &["commit", "-m", "fixture"]); - fs::write(self.path.join("src/lib.rs"), "pub fn changed() {}\n").unwrap(); - fs::write( - self.path.join("Cargo.toml"), - "[package]\nname = \"fixture\"\nversion = \"0.1.0\"\n", - ) - .unwrap(); - } -} - -impl Drop for FixtureRepo { - fn drop(&mut self) { - let _ = fs::remove_dir_all(&self.path); - } -} - -fn git(repo: &Path, args: &[&str]) { - let output = Command::new("git") - .arg("-C") - .arg(repo) - .args(args) - .output() - .unwrap(); - assert!( - output.status.success(), - "{}", - String::from_utf8_lossy(&output.stderr) - ); -} From 697d5d3cc816f8afe7bf78b74f1063075c505b8d Mon Sep 17 00:00:00 2001 From: Goudarz Firoozi <63324407+greygoody@users.noreply.github.com> Date: Thu, 23 Jul 2026 20:25:40 +0330 Subject: [PATCH 11/24] test(cli): replace legacy session command coverage --- .../codectx-cli/tests/cli_command_contract.rs | 390 ------------------ 1 file changed, 390 deletions(-) delete mode 100644 crates/codectx-cli/tests/cli_command_contract.rs diff --git a/crates/codectx-cli/tests/cli_command_contract.rs b/crates/codectx-cli/tests/cli_command_contract.rs deleted file mode 100644 index 1bb912c..0000000 --- a/crates/codectx-cli/tests/cli_command_contract.rs +++ /dev/null @@ -1,390 +0,0 @@ -use serde_json::Value; -use std::{ - env, fs, io, - path::{Path, PathBuf}, - process::{Command, Output}, - time::{SystemTime, UNIX_EPOCH}, -}; - -const CURRENT_TOML: &str = r#"[session] -id = "cli-contract-test" -goal = "Validate codectx CLI command behavior" -status = "active" - -[scope] -allowed_paths = ["src/**"] -avoid_paths = [] - -[scope.net_moves] -allowed_if_reported = ["Cargo.toml"] -reason_required = true - -[evidence] -required = ["cargo test --workspace"] - -[exit] -done_when = [] -kill_when = [] -"#; - -#[test] -fn snapshot_surface_net_move_and_prove_record_work_together() { - let repo = FixtureRepo::new("snapshot-surface-net-move-prove"); - - let snapshot = codectx(&repo, ["snapshot", "--json"]).success_json(); - assert_eq!(snapshot["session"]["id"], "cli-contract-test"); - assert_json_array_contains(&snapshot["changes"]["changed_files"], "src/lib.rs"); - assert_json_array_contains(&snapshot["changes"]["changed_files"], "Cargo.toml"); - assert_json_array_contains(&snapshot["scope"]["violations"], "Cargo.toml"); - assert_json_array_contains(&snapshot["scope"]["unexplained_violations"], "Cargo.toml"); - - let surface = codectx(&repo, ["surface"]).success_stdout(); - for expected in [ - "Codectx HUD", - "Status:", - "Mission:", - "Boundary:", - "Proof:", - "Next:", - "cargo test --workspace", - ] { - assert!( - surface.contains(expected), - "stdout did not contain {expected:?}\nstdout:\n{surface}" - ); - } - - let net_move = codectx( - &repo, - [ - "net-move", - "Cargo.toml", - "--reason", - "Manifest version changed for this fixture slice", - ], - ) - .success_stdout(); - assert!( - net_move.contains("recorded net move: Cargo.toml"), - "stdout:\n{net_move}" - ); - - let proof = codectx( - &repo, - [ - "prove", - "record", - "--command", - "cargo test --workspace", - "--status", - "passed", - ], - ) - .success_stdout(); - assert!( - proof.contains("recorded proof: cargo test --workspace"), - "stdout:\n{proof}" - ); - - let events = fs::read_to_string(repo.path().join(".codectx/runtime/events.jsonl")) - .expect("read runtime events"); - assert!(events.contains("net_move_declared"), "events:\n{events}"); - assert!(events.contains("proof_recorded"), "events:\n{events}"); - - let snapshot = codectx(&repo, ["snapshot", "--json"]).success_json(); - assert_json_array_contains_path(&snapshot["scope"]["net_moves"], "Cargo.toml"); - assert_eq!(snapshot["runtime"]["event_count"], 2); - assert_json_array_contains( - &snapshot["surface"]["proof"]["passed"], - "cargo test --workspace", - ); -} - -#[test] -fn context_views_accept_documented_view_arguments() { - let repo = FixtureRepo::new("context-views"); - - for view in ["summary", "default", "complete"] { - let stdout = codectx(&repo, ["context", "--view", view]).success_stdout(); - assert!( - stdout.contains("context view:"), - "view {view:?} stdout:\n{stdout}" - ); - } - - let output = codectx(&repo, ["context", "--view", "summary", "--json"]).success(); - serde_json::from_slice::(&output.stdout).expect("context summary JSON"); -} - -#[test] -fn done_is_declared_but_not_implemented_in_0_1_0() { - let repo = FixtureRepo::new("done-not-implemented"); - - let stderr = codectx(&repo, ["done"]).failure_stderr(); - - assert!( - stderr.contains("`codectx done` is not implemented yet"), - "stderr:\n{stderr}" - ); -} - -#[test] -fn net_move_rejects_invalid_input() { - let repo = FixtureRepo::new("net-move-invalid-input"); - - let empty_reason = codectx(&repo, ["net-move", "Cargo.toml", "--reason", " "]).failure_stderr(); - assert!( - empty_reason.contains("net-move reason must not be empty"), - "stderr:\n{empty_reason}" - ); - - let parent_path = codectx( - &repo, - [ - "net-move", - "../outside.txt", - "--reason", - "Trying to escape the repo", - ], - ) - .failure_stderr(); - assert!( - parent_path.contains("net-move path must stay inside the repository"), - "stderr:\n{parent_path}" - ); -} - -#[test] -fn prove_record_rejects_empty_command() { - let repo = FixtureRepo::new("prove-record-empty-command"); - - let stderr = codectx( - &repo, - ["prove", "record", "--command", " ", "--status", "passed"], - ) - .failure_stderr(); - assert!( - stderr.contains("proof command must not be empty"), - "stderr:\n{stderr}" - ); -} - -#[test] -fn ok_reports_blocked_context_in_human_and_json_forms() { - let repo = FixtureRepo::new("ok-blocked-context"); - - let stdout = codectx(&repo, ["ok"]).failure_stdout(); - - assert!(stdout.contains("not ok: blocked"), "stdout:\n{stdout}"); - assert!(stdout.contains("Missing proof:"), "stdout:\n{stdout}"); - assert!( - stdout.contains("cargo test --workspace"), - "stdout:\n{stdout}" - ); - assert!( - stdout.contains("Unexplained violations:"), - "stdout:\n{stdout}" - ); - assert!(stdout.contains("Cargo.toml"), "stdout:\n{stdout}"); - - let output = codectx(&repo, ["ok", "--json"]).failure(); - let json: Value = serde_json::from_slice(&output.stdout).unwrap_or_else(|error| { - panic!( - "stdout should be valid JSON: {error}\nstdout:\n{}", - String::from_utf8_lossy(&output.stdout) - ) - }); - - assert_eq!(json["status"], "blocked"); - assert_json_array_contains(&json["missing_proof"], "cargo test --workspace"); - assert_json_array_contains(&json["unexplained_violations"], "Cargo.toml"); - assert_eq!(json["net_moves_review_required"], 0); -} - -fn codectx(repo: &FixtureRepo, args: I) -> CommandOutput -where - I: IntoIterator, - S: AsRef, -{ - CommandOutput { - output: Command::new(env!("CARGO_BIN_EXE_codectx")) - .args(args) - .current_dir(repo.path()) - .output() - .expect("run codectx"), - } -} - -fn assert_json_array_contains(value: &Value, expected: &str) { - let array = value.as_array().expect("JSON value should be an array"); - assert!( - array.iter().any(|item| item == expected), - "array did not contain {expected:?}: {array:?}" - ); -} - -fn assert_json_array_contains_path(value: &Value, expected_path: &str) { - let array = value.as_array().expect("JSON value should be an array"); - assert!( - array - .iter() - .any(|item| item.get("path").and_then(Value::as_str) == Some(expected_path)), - "array did not contain path {expected_path:?}: {array:?}" - ); -} - -struct CommandOutput { - output: Output, -} - -impl CommandOutput { - fn success(self) -> Output { - assert!( - self.output.status.success(), - "command failed\nstdout:\n{}\nstderr:\n{}", - String::from_utf8_lossy(&self.output.stdout), - String::from_utf8_lossy(&self.output.stderr) - ); - self.output - } - - fn success_stdout(self) -> String { - let output = self.success(); - String::from_utf8(output.stdout).expect("stdout should be UTF-8") - } - - fn success_json(self) -> Value { - let stdout = self.success_stdout(); - serde_json::from_str(&stdout).unwrap_or_else(|error| { - panic!("stdout should be valid JSON: {error}\nstdout:\n{stdout}") - }) - } - - fn failure(self) -> Output { - assert!( - !self.output.status.success(), - "command unexpectedly succeeded\nstdout:\n{}\nstderr:\n{}", - String::from_utf8_lossy(&self.output.stdout), - String::from_utf8_lossy(&self.output.stderr) - ); - self.output - } - - fn failure_stdout(self) -> String { - let output = self.failure(); - String::from_utf8(output.stdout).expect("stdout should be UTF-8") - } - - fn failure_stderr(self) -> String { - let output = self.failure(); - String::from_utf8(output.stderr).expect("stderr should be UTF-8") - } -} - -struct FixtureRepo { - path: PathBuf, -} - -impl FixtureRepo { - fn new(name: &str) -> Self { - let repo = Self { - path: unique_temp_path(name), - }; - repo.init(); - repo - } - - fn path(&self) -> &Path { - &self.path - } - - fn init(&self) { - fs::create_dir_all(self.path.join("src")).expect("create src"); - fs::create_dir_all(self.path.join(".codectx")).expect("create .codectx"); - fs::write( - self.path.join("Cargo.toml"), - r#"[package] -name = "codectx-cli-contract-fixture" -version = "0.1.0" -edition = "2021" - -[lib] -path = "src/lib.rs" -"#, - ) - .expect("write Cargo.toml"); - fs::write( - self.path.join("src/lib.rs"), - "pub fn fixture() -> u8 { 1 }\n", - ) - .expect("write src/lib.rs"); - fs::write(self.path.join(".codectx/current.toml"), CURRENT_TOML) - .expect("write current.toml"); - - git(self.path(), ["init"]); - git( - self.path(), - ["config", "user.email", "codectx@example.invalid"], - ); - git(self.path(), ["config", "user.name", "Codectx Test"]); - git(self.path(), ["add", "."]); - git(self.path(), ["commit", "-m", "baseline fixture"]); - - fs::write( - self.path.join("src/lib.rs"), - "pub fn fixture() -> u8 { 2 }\n", - ) - .expect("modify src/lib.rs"); - fs::write( - self.path.join("Cargo.toml"), - r#"[package] -name = "codectx-cli-contract-fixture" -version = "0.1.1" -edition = "2021" - -[lib] -path = "src/lib.rs" -"#, - ) - .expect("modify Cargo.toml"); - } -} - -impl Drop for FixtureRepo { - fn drop(&mut self) { - match fs::remove_dir_all(&self.path) { - Ok(()) => {} - Err(error) if error.kind() == io::ErrorKind::NotFound => {} - Err(error) => panic!("remove fixture repo {}: {error}", self.path.display()), - } - } -} - -fn unique_temp_path(name: &str) -> PathBuf { - let nanos = SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("system time should be after epoch") - .as_nanos(); - env::temp_dir().join(format!( - "codectx-cli-contract-{name}-{}-{nanos}", - std::process::id() - )) -} - -fn git(repo: &Path, args: I) -where - I: IntoIterator, - S: AsRef, -{ - let output = Command::new("git") - .args(args) - .current_dir(repo) - .output() - .expect("run git"); - assert!( - output.status.success(), - "git command failed\nstdout:\n{}\nstderr:\n{}", - String::from_utf8_lossy(&output.stdout), - String::from_utf8_lossy(&output.stderr) - ); -} From b84bb8669fda1c95720f66deaf52077d53c33624 Mon Sep 17 00:00:00 2001 From: Goudarz Firoozi <63324407+greygoody@users.noreply.github.com> Date: Thu, 23 Jul 2026 20:26:21 +0330 Subject: [PATCH 12/24] test(cli): prove synthetic Nix checkpoint invalidation --- crates/codectx-cli/tests/nix_cli_contract.rs | 192 +++++++++++++++++++ 1 file changed, 192 insertions(+) create mode 100644 crates/codectx-cli/tests/nix_cli_contract.rs diff --git a/crates/codectx-cli/tests/nix_cli_contract.rs b/crates/codectx-cli/tests/nix_cli_contract.rs new file mode 100644 index 0000000..f315dfd --- /dev/null +++ b/crates/codectx-cli/tests/nix_cli_contract.rs @@ -0,0 +1,192 @@ +use serde_json::Value; +use std::{ + env, fs, + path::{Path, PathBuf}, + process::{Command, Output}, + time::{SystemTime, UNIX_EPOCH}, +}; + +#[test] +fn root_help_exposes_only_the_nix_product_surface() { + let output = Command::new(env!("CARGO_BIN_EXE_codectx")) + .arg("--help") + .output() + .expect("run codectx help"); + assert_success(&output); + let stdout = String::from_utf8(output.stdout).unwrap(); + + assert!(stdout.contains("Verify and resume bounded NixOS configuration changes")); + assert!(stdout.contains("nix")); + assert!(!stdout.contains("handoff")); + assert!(!stdout.contains("runseal")); + assert!(!stdout.contains("legacy")); +} + +#[cfg(unix)] +#[test] +fn inspect_checkpoint_and_resume_invalidate_changed_nix_inputs() { + use std::os::unix::fs::PermissionsExt; + + let fixture = FixtureRepo::new(); + fixture.write_fake_nix("/nix/store/aaa-nixos-system-example"); + + let inspect = fixture.codectx(["nix", "inspect", "--json"]); + assert_success(&inspect); + let inspect: Value = serde_json::from_slice(&inspect.stdout).unwrap(); + assert_eq!( + inspect["candidate_system"], + "/nix/store/aaa-nixos-system-example" + ); + assert_eq!(inspect["changed_paths"][0]["relevance"], "expected"); + assert!(inspect["flake_lock_digest"].as_str().is_some()); + + let checkpoint = fixture.codectx(["nix", "checkpoint", "--json"]); + assert_success(&checkpoint); + let checkpoint: Value = serde_json::from_slice(&checkpoint.stdout).unwrap(); + assert_eq!(checkpoint["schema_version"], 1); + + fs::write(fixture.path.join("flake.lock"), "changed lock\n").unwrap(); + fixture.write_fake_nix("/nix/store/bbb-nixos-system-example"); + + let resume = fixture.codectx(["nix", "resume", "--json"]); + assert_success(&resume); + let resume: Value = serde_json::from_slice(&resume.stdout).unwrap(); + let invalidations = resume["invalidated_context"].as_array().unwrap(); + + assert!(invalidations + .iter() + .any(|item| item["code"] == "flake_lock_changed")); + assert!(invalidations + .iter() + .any(|item| item["code"] == "candidate_system_changed")); + assert_eq!( + resume["next_probe"], + "re-evaluate and rebuild the target before trusting prior evidence" + ); + + let mode = fs::metadata(fixture.fake_nix_path()).unwrap().permissions().mode(); + assert_ne!(mode & 0o111, 0); +} + +struct FixtureRepo { + path: PathBuf, + fake_bin: PathBuf, +} + +impl FixtureRepo { + fn new() -> Self { + let nanos = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos(); + let path = env::temp_dir().join(format!( + "codectx-nix-cli-contract-{}-{nanos}", + std::process::id() + )); + let fake_bin = path.join("fake-bin"); + let fixture = Self { path, fake_bin }; + fixture.init(); + fixture + } + + fn init(&self) { + fs::create_dir_all(self.path.join("hosts/example")).unwrap(); + fs::create_dir_all(self.path.join(".codectx")).unwrap(); + fs::create_dir_all(&self.fake_bin).unwrap(); + fs::write(self.path.join("flake.nix"), "{ outputs = _: {}; }\n").unwrap(); + fs::write(self.path.join("flake.lock"), "initial lock\n").unwrap(); + fs::write( + self.path.join("hosts/example/configuration.nix"), + "{ ... }: {}\n", + ) + .unwrap(); + fs::write( + self.path.join(".codectx/nix.toml"), + r#"id = "NX-TEST" +target = ".#nixosConfigurations.example.config.system.build.toplevel" +flake_lock_path = "flake.lock" +baseline = "HEAD" +expected_paths = ["hosts/example/**"] +supporting_paths = ["flake.nix", "flake.lock"] +constraints = ["preserve remote access"] +checks = ["nix flake check"] +"#, + ) + .unwrap(); + + git(&self.path, ["init", "-q"]); + git( + &self.path, + ["config", "user.email", "codectx@example.invalid"], + ); + git(&self.path, ["config", "user.name", "Codectx Test"]); + git(&self.path, ["add", "."]); + git(&self.path, ["commit", "-q", "-m", "fixture"]); + + fs::write( + self.path.join("hosts/example/configuration.nix"), + "{ ... }: { services.openssh.enable = true; }\n", + ) + .unwrap(); + } + + #[cfg(unix)] + fn write_fake_nix(&self, store_path: &str) { + use std::os::unix::fs::PermissionsExt; + + let script = format!("#!/bin/sh\nprintf '%s\\n' '{store_path}'\n"); + fs::write(self.fake_nix_path(), script).unwrap(); + let mut permissions = fs::metadata(self.fake_nix_path()).unwrap().permissions(); + permissions.set_mode(0o755); + fs::set_permissions(self.fake_nix_path(), permissions).unwrap(); + } + + fn fake_nix_path(&self) -> PathBuf { + self.fake_bin.join("nix") + } + + fn codectx(&self, args: [&str; N]) -> Output { + let existing_path = env::var_os("PATH").unwrap_or_default(); + let joined = env::join_paths( + std::iter::once(self.fake_bin.as_os_str()).chain(env::split_paths(&existing_path).map(|p| { + Box::leak(p.into_os_string().into_boxed_os_str()) as &std::ffi::OsStr + })), + ) + .unwrap(); + + Command::new(env!("CARGO_BIN_EXE_codectx")) + .args(args) + .current_dir(&self.path) + .env("PATH", joined) + .output() + .expect("run codectx") + } +} + +impl Drop for FixtureRepo { + fn drop(&mut self) { + let _ = fs::remove_dir_all(&self.path); + } +} + +fn git(repo: &Path, args: I) +where + I: IntoIterator, + S: AsRef, +{ + let output = Command::new("git") + .args(args) + .current_dir(repo) + .output() + .expect("run git"); + assert_success(&output); +} + +fn assert_success(output: &Output) { + assert!( + output.status.success(), + "command failed\nstdout:\n{}\nstderr:\n{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); +} From 8225cb9a426a6ad1b70e587b376fb9265f4719a5 Mon Sep 17 00:00:00 2001 From: Goudarz Firoozi <63324407+greygoody@users.noreply.github.com> Date: Thu, 23 Jul 2026 20:27:11 +0330 Subject: [PATCH 13/24] test(cli): simplify synthetic Nix PATH fixture --- crates/codectx-cli/tests/nix_cli_contract.rs | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/crates/codectx-cli/tests/nix_cli_contract.rs b/crates/codectx-cli/tests/nix_cli_contract.rs index f315dfd..7f4883f 100644 --- a/crates/codectx-cli/tests/nix_cli_contract.rs +++ b/crates/codectx-cli/tests/nix_cli_contract.rs @@ -25,8 +25,6 @@ fn root_help_exposes_only_the_nix_product_surface() { #[cfg(unix)] #[test] fn inspect_checkpoint_and_resume_invalidate_changed_nix_inputs() { - use std::os::unix::fs::PermissionsExt; - let fixture = FixtureRepo::new(); fixture.write_fake_nix("/nix/store/aaa-nixos-system-example"); @@ -63,9 +61,6 @@ fn inspect_checkpoint_and_resume_invalidate_changed_nix_inputs() { resume["next_probe"], "re-evaluate and rebuild the target before trusting prior evidence" ); - - let mode = fs::metadata(fixture.fake_nix_path()).unwrap().permissions().mode(); - assert_ne!(mode & 0o111, 0); } struct FixtureRepo { @@ -147,12 +142,9 @@ checks = ["nix flake check"] fn codectx(&self, args: [&str; N]) -> Output { let existing_path = env::var_os("PATH").unwrap_or_default(); - let joined = env::join_paths( - std::iter::once(self.fake_bin.as_os_str()).chain(env::split_paths(&existing_path).map(|p| { - Box::leak(p.into_os_string().into_boxed_os_str()) as &std::ffi::OsStr - })), - ) - .unwrap(); + let mut paths = vec![self.fake_bin.clone()]; + paths.extend(env::split_paths(&existing_path)); + let joined = env::join_paths(paths).unwrap(); Command::new(env!("CARGO_BIN_EXE_codectx")) .args(args) From de6900cf23b23c6ac7a0bf68d734e762c0830b65 Mon Sep 17 00:00:00 2001 From: Goudarz Firoozi <63324407+greygoody@users.noreply.github.com> Date: Thu, 23 Jul 2026 20:27:19 +0330 Subject: [PATCH 14/24] chore: replace context compiler proof with Nix checkpoint proof --- justfile | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/justfile b/justfile index 09ff048..22e3c57 100644 --- a/justfile +++ b/justfile @@ -3,21 +3,12 @@ check: cargo clippy --workspace --all-targets -- -D warnings cargo test --workspace cargo run -p codectx-cli --bin codectx -- --help - bash scripts/demo.sh codectx *args: cargo run -p codectx-cli --bin codectx -- {{args}} -codectx-legacy *args: - cargo run -p codectx-cli --bin codectx-legacy -- {{args}} - -codectx-bins: - cargo build -p codectx-cli --bins - @echo ./target/debug/codectx - @echo ./target/debug/codectx-legacy - -context-packet-proof: +nix-proof: cargo fmt --all --check - cargo test -p codectx-core --test context_packet_contract -- --nocapture - cargo test -p codectx-cli --test context_packet_contract -- --nocapture - bash scripts/demo.sh + cargo test -p codectx-core nix::tests -- --nocapture + cargo test -p codectx-cli --test nix_cli_contract -- --nocapture + cargo run -p codectx-cli --bin codectx -- --help From a4014a1f141439b0a3f042553148d81e3a242fbb Mon Sep 17 00:00:00 2001 From: Goudarz Firoozi <63324407+greygoody@users.noreply.github.com> Date: Thu, 23 Jul 2026 20:27:48 +0330 Subject: [PATCH 15/24] chore(cli): preserve package version during product migration --- crates/codectx-cli/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/codectx-cli/Cargo.toml b/crates/codectx-cli/Cargo.toml index a618786..a211b0b 100644 --- a/crates/codectx-cli/Cargo.toml +++ b/crates/codectx-cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "codectx-cli" -version = "0.2.0" +version = "0.1.0" edition.workspace = true license.workspace = true repository.workspace = true From b23016b312cea91b0ca9b9cf704ee2d2ad7328f5 Mon Sep 17 00:00:00 2001 From: Goudarz Firoozi <63324407+greygoody@users.noreply.github.com> Date: Thu, 23 Jul 2026 20:28:07 +0330 Subject: [PATCH 16/24] ci: verify Rust workspace and Nix checkpoint CLI --- .github/workflows/ci.yml | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..2710f52 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,22 @@ +name: ci + +on: + pull_request: + push: + branches: [main] + +permissions: + contents: read + +jobs: + rust: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt, clippy + - run: cargo fmt --all --check + - run: cargo clippy --workspace --all-targets -- -D warnings + - run: cargo test --workspace + - run: cargo run -p codectx-cli --bin codectx -- --help From f7b67e0f3cb994c0af7d2152f58f90b7de70e84b Mon Sep 17 00:00:00 2001 From: Goudarz Firoozi <63324407+greygoody@users.noreply.github.com> Date: Thu, 23 Jul 2026 20:30:24 +0330 Subject: [PATCH 17/24] ci: compile and export rustfmt output for migration branch --- .github/workflows/proof.yml | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/.github/workflows/proof.yml b/.github/workflows/proof.yml index 8548b79..627d684 100644 --- a/.github/workflows/proof.yml +++ b/.github/workflows/proof.yml @@ -25,8 +25,19 @@ jobs: - name: Cache Cargo uses: Swatinem/rust-cache@v2 - - name: Format - run: cargo fmt --all --check + - name: Format working tree + run: cargo fmt --all + + - name: Upload formatted sources + uses: actions/upload-artifact@v4 + with: + name: formatted-sources + path: | + crates/codectx-core/src/nix.rs + crates/codectx-core/src/error.rs + crates/codectx-core/src/lib.rs + crates/codectx-cli/src/context_main.rs + crates/codectx-cli/tests/nix_cli_contract.rs - name: Clippy run: cargo clippy --workspace --all-targets -- -D warnings @@ -36,6 +47,3 @@ jobs: - name: CLI help run: cargo run -p codectx-cli --bin codectx -- --help - - - name: Demo - run: bash scripts/demo.sh From ceffef5a3203d54526f0d552273ed20e71222d4b Mon Sep 17 00:00:00 2001 From: Goudarz Firoozi <63324407+greygoody@users.noreply.github.com> Date: Thu, 23 Jul 2026 20:30:40 +0330 Subject: [PATCH 18/24] ci: keep one canonical proof workflow --- .github/workflows/ci.yml | 22 ---------------------- 1 file changed, 22 deletions(-) delete mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml deleted file mode 100644 index 2710f52..0000000 --- a/.github/workflows/ci.yml +++ /dev/null @@ -1,22 +0,0 @@ -name: ci - -on: - pull_request: - push: - branches: [main] - -permissions: - contents: read - -jobs: - rust: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@stable - with: - components: rustfmt, clippy - - run: cargo fmt --all --check - - run: cargo clippy --workspace --all-targets -- -D warnings - - run: cargo test --workspace - - run: cargo run -p codectx-cli --bin codectx -- --help From 7dd190ed99056f323e2499daf01e88b100f40e2a Mon Sep 17 00:00:00 2001 From: Goudarz Firoozi <63324407+greygoody@users.noreply.github.com> Date: Thu, 23 Jul 2026 20:31:55 +0330 Subject: [PATCH 19/24] ci: export failing test output during Nix migration --- .github/workflows/proof.yml | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/.github/workflows/proof.yml b/.github/workflows/proof.yml index 627d684..942003b 100644 --- a/.github/workflows/proof.yml +++ b/.github/workflows/proof.yml @@ -43,7 +43,17 @@ jobs: run: cargo clippy --workspace --all-targets -- -D warnings - name: Test - run: cargo test --workspace + shell: bash + run: | + set -o pipefail + cargo test --workspace 2>&1 | tee test.log + + - name: Upload test log + if: always() + uses: actions/upload-artifact@v4 + with: + name: test-log + path: test.log - name: CLI help run: cargo run -p codectx-cli --bin codectx -- --help From b129eb74552d73292f507076c0e61ad18f7936e8 Mon Sep 17 00:00:00 2001 From: Goudarz Firoozi <63324407+greygoody@users.noreply.github.com> Date: Thu, 23 Jul 2026 20:33:43 +0330 Subject: [PATCH 20/24] test(cli): assert the expected Nix host path explicitly --- crates/codectx-cli/tests/nix_cli_contract.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/crates/codectx-cli/tests/nix_cli_contract.rs b/crates/codectx-cli/tests/nix_cli_contract.rs index 7f4883f..7092a4f 100644 --- a/crates/codectx-cli/tests/nix_cli_contract.rs +++ b/crates/codectx-cli/tests/nix_cli_contract.rs @@ -35,7 +35,14 @@ fn inspect_checkpoint_and_resume_invalidate_changed_nix_inputs() { inspect["candidate_system"], "/nix/store/aaa-nixos-system-example" ); - assert_eq!(inspect["changed_paths"][0]["relevance"], "expected"); + assert!(inspect["changed_paths"] + .as_array() + .unwrap() + .iter() + .any(|change| { + change["path"] == "hosts/example/configuration.nix" + && change["relevance"] == "expected" + })); assert!(inspect["flake_lock_digest"].as_str().is_some()); let checkpoint = fixture.codectx(["nix", "checkpoint", "--json"]); From 67f2002da6786863bcc23b11b858a21d15d0dea6 Mon Sep 17 00:00:00 2001 From: Goudarz Firoozi <63324407+greygoody@users.noreply.github.com> Date: Thu, 23 Jul 2026 20:36:00 +0330 Subject: [PATCH 21/24] build(cli): disable legacy main auto-discovery --- crates/codectx-cli/Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/codectx-cli/Cargo.toml b/crates/codectx-cli/Cargo.toml index a211b0b..266359f 100644 --- a/crates/codectx-cli/Cargo.toml +++ b/crates/codectx-cli/Cargo.toml @@ -4,6 +4,7 @@ version = "0.1.0" edition.workspace = true license.workspace = true repository.workspace = true +autobins = false [[bin]] name = "codectx" From 12cd6cc0dbded14c75b8b2574aa56677dda135ec Mon Sep 17 00:00:00 2001 From: Goudarz Firoozi <63324407+greygoody@users.noreply.github.com> Date: Thu, 23 Jul 2026 20:37:57 +0330 Subject: [PATCH 22/24] ci: commit rustfmt output for migration branch --- .github/workflows/proof.yml | 41 +++++++++++++++---------------------- 1 file changed, 17 insertions(+), 24 deletions(-) diff --git a/.github/workflows/proof.yml b/.github/workflows/proof.yml index 942003b..d59fe02 100644 --- a/.github/workflows/proof.yml +++ b/.github/workflows/proof.yml @@ -8,14 +8,16 @@ on: pull_request: permissions: - contents: read + contents: write jobs: proof: runs-on: ubuntu-latest steps: - - name: Checkout + - name: Checkout branch uses: actions/checkout@v4 + with: + ref: ${{ github.head_ref || github.ref_name }} - name: Install Rust uses: dtolnay/rust-toolchain@stable @@ -25,35 +27,26 @@ jobs: - name: Cache Cargo uses: Swatinem/rust-cache@v2 - - name: Format working tree + - name: Format run: cargo fmt --all - - name: Upload formatted sources - uses: actions/upload-artifact@v4 - with: - name: formatted-sources - path: | - crates/codectx-core/src/nix.rs - crates/codectx-core/src/error.rs - crates/codectx-core/src/lib.rs - crates/codectx-cli/src/context_main.rs - crates/codectx-cli/tests/nix_cli_contract.rs + - name: Commit formatting + if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository + shell: bash + run: | + if ! git diff --quiet; then + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add crates/codectx-core/src crates/codectx-cli/src crates/codectx-cli/tests + git commit -m "style: apply rustfmt" + git push origin HEAD:${{ github.head_ref }} + fi - name: Clippy run: cargo clippy --workspace --all-targets -- -D warnings - name: Test - shell: bash - run: | - set -o pipefail - cargo test --workspace 2>&1 | tee test.log - - - name: Upload test log - if: always() - uses: actions/upload-artifact@v4 - with: - name: test-log - path: test.log + run: cargo test --workspace - name: CLI help run: cargo run -p codectx-cli --bin codectx -- --help From 829a1d85f0e2e31c650254e98eaf0a8598589781 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 23 Jul 2026 17:08:40 +0000 Subject: [PATCH 23/24] style: apply rustfmt --- crates/codectx-cli/tests/nix_cli_contract.rs | 3 +-- crates/codectx-core/src/lib.rs | 6 +++--- crates/codectx-core/src/nix.rs | 22 +++++++------------- 3 files changed, 11 insertions(+), 20 deletions(-) diff --git a/crates/codectx-cli/tests/nix_cli_contract.rs b/crates/codectx-cli/tests/nix_cli_contract.rs index 7092a4f..d49f696 100644 --- a/crates/codectx-cli/tests/nix_cli_contract.rs +++ b/crates/codectx-cli/tests/nix_cli_contract.rs @@ -40,8 +40,7 @@ fn inspect_checkpoint_and_resume_invalidate_changed_nix_inputs() { .unwrap() .iter() .any(|change| { - change["path"] == "hosts/example/configuration.nix" - && change["relevance"] == "expected" + change["path"] == "hosts/example/configuration.nix" && change["relevance"] == "expected" })); assert!(inspect["flake_lock_digest"].as_str().is_some()); diff --git a/crates/codectx-core/src/lib.rs b/crates/codectx-core/src/lib.rs index 83d759c..ac27e71 100644 --- a/crates/codectx-core/src/lib.rs +++ b/crates/codectx-core/src/lib.rs @@ -35,9 +35,9 @@ pub use lens::{enter_task, read_context, IncludedFile, LensStatus, ProofCommand, pub use nix::{ assess_nix_resume, classify_nix_paths, render_nix_observation, render_nix_resume, CommandNixProbe, NixChangedPath, NixCheckpoint, NixCheckpointNotes, NixContextBuilder, - NixEvidence, NixInvalidation, NixObservation, NixPathRelevance, NixProbe, - NixRepositoryState, NixResumeAssessment, NixStaleEvidence, NixTargetSpec, - DEFAULT_NIX_CHECKPOINT_PATH, DEFAULT_NIX_SPEC_PATH, NIX_CHECKPOINT_SCHEMA_VERSION, + NixEvidence, NixInvalidation, NixObservation, NixPathRelevance, NixProbe, NixRepositoryState, + NixResumeAssessment, NixStaleEvidence, NixTargetSpec, DEFAULT_NIX_CHECKPOINT_PATH, + DEFAULT_NIX_SPEC_PATH, NIX_CHECKPOINT_SCHEMA_VERSION, }; pub use packet::{ classify_changes, render_context_hud, render_review, write_context_packet, AttentionItem, diff --git a/crates/codectx-core/src/nix.rs b/crates/codectx-core/src/nix.rs index 0d51e89..75d6b2c 100644 --- a/crates/codectx-core/src/nix.rs +++ b/crates/codectx-core/src/nix.rs @@ -206,11 +206,7 @@ pub struct NixResumeAssessment { } pub trait NixProbe { - fn evaluate_toplevel( - &self, - repo_root: &Utf8Path, - target: &str, - ) -> Result>; + fn evaluate_toplevel(&self, repo_root: &Utf8Path, target: &str) -> Result>; fn current_system(&self) -> Result>; } @@ -218,11 +214,7 @@ pub trait NixProbe { pub struct CommandNixProbe; impl NixProbe for CommandNixProbe { - fn evaluate_toplevel( - &self, - repo_root: &Utf8Path, - target: &str, - ) -> Result> { + fn evaluate_toplevel(&self, repo_root: &Utf8Path, target: &str) -> Result> { let output = Command::new("nix") .arg("eval") .arg("--raw") @@ -432,7 +424,10 @@ pub fn render_nix_observation(spec: &NixTargetSpec, observation: &NixObservation output.push_str(&format!("Repository: {}\n", observation.repository.head)); output.push_str(&format!( "flake.lock: {}\n", - observation.flake_lock_digest.as_deref().unwrap_or("missing") + observation + .flake_lock_digest + .as_deref() + .unwrap_or("missing") )); output.push_str(&format!( "Active system: {}\n", @@ -462,10 +457,7 @@ pub fn render_nix_observation(spec: &NixTargetSpec, observation: &NixObservation pub fn render_nix_resume(assessment: &NixResumeAssessment) -> String { let mut output = String::new(); output.push_str("CODECTX · nixos resume\n\n"); - output.push_str(&format!( - "Checkpoint: {}\n", - assessment.checkpoint_digest - )); + output.push_str(&format!("Checkpoint: {}\n", assessment.checkpoint_digest)); if assessment.invalidated_context.is_empty() { output.push_str("Invalidated context: none\n"); From a8a76908653aa259c0e4606c153eb79a3f116052 Mon Sep 17 00:00:00 2001 From: Goudarz Firoozi <63324407+greygoody@users.noreply.github.com> Date: Thu, 23 Jul 2026 20:39:08 +0330 Subject: [PATCH 24/24] ci: restore strict read-only proof gate --- .github/workflows/proof.yml | 20 +++----------------- 1 file changed, 3 insertions(+), 17 deletions(-) diff --git a/.github/workflows/proof.yml b/.github/workflows/proof.yml index d59fe02..a135e13 100644 --- a/.github/workflows/proof.yml +++ b/.github/workflows/proof.yml @@ -8,16 +8,14 @@ on: pull_request: permissions: - contents: write + contents: read jobs: proof: runs-on: ubuntu-latest steps: - - name: Checkout branch + - name: Checkout uses: actions/checkout@v4 - with: - ref: ${{ github.head_ref || github.ref_name }} - name: Install Rust uses: dtolnay/rust-toolchain@stable @@ -28,19 +26,7 @@ jobs: uses: Swatinem/rust-cache@v2 - name: Format - run: cargo fmt --all - - - name: Commit formatting - if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository - shell: bash - run: | - if ! git diff --quiet; then - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add crates/codectx-core/src crates/codectx-cli/src crates/codectx-cli/tests - git commit -m "style: apply rustfmt" - git push origin HEAD:${{ github.head_ref }} - fi + run: cargo fmt --all --check - name: Clippy run: cargo clippy --workspace --all-targets -- -D warnings