From fc8ad7b3a85c4530bb885bd17e19ec00b5a3338e Mon Sep 17 00:00:00 2001 From: Hunter Bown Date: Wed, 3 Jun 2026 12:16:06 -0700 Subject: [PATCH] feat(project): enrich repo constitution (invariants, branch policy, escalation) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per the layered-authority clarification (base myth → global Constitution → repo constitution = local law → task packet → runtime policy), extend .codewhale/constitution.json beyond authority+verification with optional: - protected_invariants — repo invariants the agent must not break - branch_policy — branch/release policy in effect - escalate_when — conditions to stop and escalate to the user All optional; rendered as concise model-facing prose. The global Brother Whale identity anchor and Constitution in prompts/base.md are unchanged (verified untouched on this branch). Dogfood constitution.json filled with CodeWhale's real invariants (prefix-cache byte-stability, transcript replay, stable Rust, cli/tui parity), branch policy (codex/v0.8.53), and escalation rules. Docs note the layered hierarchy. cargo test -p codewhale-tui --bins → 3946 passed; clippy clean. --- .codewhale/constitution.json | 14 +++++++++- crates/tui/src/project_context.rs | 46 ++++++++++++++++++++++++++++--- docs/CONFIGURATION.md | 22 +++++++++++++-- 3 files changed, 74 insertions(+), 8 deletions(-) diff --git a/.codewhale/constitution.json b/.codewhale/constitution.json index 2dbae689..6c9de07d 100644 --- a/.codewhale/constitution.json +++ b/.codewhale/constitution.json @@ -8,11 +8,23 @@ "memory", "previous-session handoffs" ], + "protected_invariants": [ + "Keep the active first-turn tool-catalog head byte-stable (DeepSeek KV prefix-cache invariant); changes to it must be one-time and deterministic.", + "Preserve old-session transcript replay: never remove a tool's registration just because it is deprecated/hidden.", + "Stable Rust only (edition 2024); no nightly features.", + "Keep the codewhale CLI dispatcher and the codewhale-tui binary in sync when crates/tui changes." + ], + "branch_policy": "v0.8.53 work targets the codex/v0.8.53 integration branch, not main. One PR per logical workstream; do not mix unrelated fixes.", "verification_policy": { "before_claiming_done": [ "run the focused tests for the changed crate (cargo test -p ), then cargo check/clippy as appropriate", "read changed files back to confirm the edit landed as intended", "never claim verification you did not perform" ] - } + }, + "escalate_when": [ + "an action is destructive or hard to reverse and was not explicitly authorized", + "changing provider/auth/config or anything that sends data to an external service", + "deleting or overwriting files you did not create, or that contradict how they were described" + ] } diff --git a/crates/tui/src/project_context.rs b/crates/tui/src/project_context.rs index 8863d78f..03cdc575 100644 --- a/crates/tui/src/project_context.rs +++ b/crates/tui/src/project_context.rs @@ -194,6 +194,15 @@ struct RepoConstitution { /// (highest authority first). #[serde(default)] authority: Option>, + /// Repo invariants the agent must not break. + #[serde(default)] + protected_invariants: Option>, + /// Branch / release policy in effect (e.g. "PRs target codex/v0.8.53"). + #[serde(default)] + branch_policy: Option, + /// Conditions under which the agent should stop and escalate to the user. + #[serde(default)] + escalate_when: Option>, #[serde(default)] verification_policy: Option, } @@ -209,7 +218,14 @@ impl RepoConstitution { /// True when the file carried no usable policy (so we can skip emitting an /// empty block). fn is_empty(&self) -> bool { - self.authority.as_ref().is_none_or(Vec::is_empty) + let list_empty = |l: &Option>| l.as_ref().is_none_or(Vec::is_empty); + list_empty(&self.authority) + && list_empty(&self.protected_invariants) + && list_empty(&self.escalate_when) + && self + .branch_policy + .as_ref() + .is_none_or(|s| s.trim().is_empty()) && self .verification_policy .as_ref() @@ -217,7 +233,8 @@ impl RepoConstitution { .is_none_or(Vec::is_empty) } - /// Render a model-facing authority block. + /// Render a model-facing authority block (concise prose, per the layered + /// model: base myth → global constitution → repo constitution = local law). fn render_block(&self, source: &Path) -> String { let mut body = String::new(); if let Some(authority) = self.authority.as_ref().filter(|a| !a.is_empty()) { @@ -228,6 +245,15 @@ impl RepoConstitution { body.push_str(&format!("{}. {item}\n", idx + 1)); } } + if let Some(invariants) = self.protected_invariants.as_ref().filter(|i| !i.is_empty()) { + body.push_str("\nProtected invariants — do not break:\n"); + for item in invariants { + body.push_str(&format!("- {item}\n")); + } + } + if let Some(policy) = self.branch_policy.as_ref().filter(|s| !s.trim().is_empty()) { + body.push_str(&format!("\nBranch / release policy: {}\n", policy.trim())); + } if let Some(steps) = self .verification_policy .as_ref() @@ -239,8 +265,14 @@ impl RepoConstitution { body.push_str(&format!("- {step}\n")); } } + if let Some(conditions) = self.escalate_when.as_ref().filter(|c| !c.is_empty()) { + body.push_str("\nStop and escalate to the user when:\n"); + for item in conditions { + body.push_str(&format!("- {item}\n")); + } + } format!( - "\nCodeWhale-specific repo authority policy (takes precedence over a legacy WHALE.md).\n\n{}", + "\nCodeWhale-specific repo authority policy (local law: subordinate to the global Constitution and the current user request, but above memory and old handoffs; takes precedence over a legacy WHALE.md).\n\n{}", source.display(), body.trim_end() ) @@ -1182,7 +1214,10 @@ mod tests { r#"{ "schema_version": 1, "authority": ["current user request", "live code and tests", "AGENTS.md"], - "verification_policy": { "before_claiming_done": ["run focused tests"] } + "protected_invariants": ["keep the tool-catalog head byte-stable"], + "branch_policy": "PRs target codex/v0.8.53, not main", + "verification_policy": { "before_claiming_done": ["run focused tests"] }, + "escalate_when": ["a destructive action was not authorized"] }"#, ) .expect("write constitution"); @@ -1195,6 +1230,9 @@ mod tests { assert!(block.contains(" **`WHALE.md` is deprecated.** It overlapped confusingly with `AGENTS.md`. > CodeWhale still **reads** an existing `WHALE.md` (below `AGENTS.md`) so old