feat(project): enrich repo constitution (invariants, branch policy, escalation)
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.
This commit is contained in:
@@ -8,11 +8,23 @@
|
|||||||
"memory",
|
"memory",
|
||||||
"previous-session handoffs"
|
"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": {
|
"verification_policy": {
|
||||||
"before_claiming_done": [
|
"before_claiming_done": [
|
||||||
"run the focused tests for the changed crate (cargo test -p <crate>), then cargo check/clippy as appropriate",
|
"run the focused tests for the changed crate (cargo test -p <crate>), then cargo check/clippy as appropriate",
|
||||||
"read changed files back to confirm the edit landed as intended",
|
"read changed files back to confirm the edit landed as intended",
|
||||||
"never claim verification you did not perform"
|
"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"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -194,6 +194,15 @@ struct RepoConstitution {
|
|||||||
/// (highest authority first).
|
/// (highest authority first).
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
authority: Option<Vec<String>>,
|
authority: Option<Vec<String>>,
|
||||||
|
/// Repo invariants the agent must not break.
|
||||||
|
#[serde(default)]
|
||||||
|
protected_invariants: Option<Vec<String>>,
|
||||||
|
/// Branch / release policy in effect (e.g. "PRs target codex/v0.8.53").
|
||||||
|
#[serde(default)]
|
||||||
|
branch_policy: Option<String>,
|
||||||
|
/// Conditions under which the agent should stop and escalate to the user.
|
||||||
|
#[serde(default)]
|
||||||
|
escalate_when: Option<Vec<String>>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
verification_policy: Option<VerificationPolicy>,
|
verification_policy: Option<VerificationPolicy>,
|
||||||
}
|
}
|
||||||
@@ -209,7 +218,14 @@ impl RepoConstitution {
|
|||||||
/// True when the file carried no usable policy (so we can skip emitting an
|
/// True when the file carried no usable policy (so we can skip emitting an
|
||||||
/// empty block).
|
/// empty block).
|
||||||
fn is_empty(&self) -> bool {
|
fn is_empty(&self) -> bool {
|
||||||
self.authority.as_ref().is_none_or(Vec::is_empty)
|
let list_empty = |l: &Option<Vec<String>>| 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
|
&& self
|
||||||
.verification_policy
|
.verification_policy
|
||||||
.as_ref()
|
.as_ref()
|
||||||
@@ -217,7 +233,8 @@ impl RepoConstitution {
|
|||||||
.is_none_or(Vec::is_empty)
|
.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 {
|
fn render_block(&self, source: &Path) -> String {
|
||||||
let mut body = String::new();
|
let mut body = String::new();
|
||||||
if let Some(authority) = self.authority.as_ref().filter(|a| !a.is_empty()) {
|
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));
|
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
|
if let Some(steps) = self
|
||||||
.verification_policy
|
.verification_policy
|
||||||
.as_ref()
|
.as_ref()
|
||||||
@@ -239,8 +265,14 @@ impl RepoConstitution {
|
|||||||
body.push_str(&format!("- {step}\n"));
|
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!(
|
format!(
|
||||||
"<codewhale_repo_constitution source=\"{}\">\nCodeWhale-specific repo authority policy (takes precedence over a legacy WHALE.md).\n\n{}</codewhale_repo_constitution>",
|
"<codewhale_repo_constitution source=\"{}\">\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{}</codewhale_repo_constitution>",
|
||||||
source.display(),
|
source.display(),
|
||||||
body.trim_end()
|
body.trim_end()
|
||||||
)
|
)
|
||||||
@@ -1182,7 +1214,10 @@ mod tests {
|
|||||||
r#"{
|
r#"{
|
||||||
"schema_version": 1,
|
"schema_version": 1,
|
||||||
"authority": ["current user request", "live code and tests", "AGENTS.md"],
|
"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");
|
.expect("write constitution");
|
||||||
@@ -1195,6 +1230,9 @@ mod tests {
|
|||||||
assert!(block.contains("<codewhale_repo_constitution"));
|
assert!(block.contains("<codewhale_repo_constitution"));
|
||||||
assert!(block.contains("current user request"));
|
assert!(block.contains("current user request"));
|
||||||
assert!(block.contains("run focused tests"));
|
assert!(block.contains("run focused tests"));
|
||||||
|
assert!(block.contains("keep the tool-catalog head byte-stable"));
|
||||||
|
assert!(block.contains("PRs target codex/v0.8.53"));
|
||||||
|
assert!(block.contains("a destructive action was not authorized"));
|
||||||
assert!(block.contains("takes precedence over a legacy WHALE.md"));
|
assert!(block.contains("takes precedence over a legacy WHALE.md"));
|
||||||
// It also surfaces through the system block.
|
// It also surfaces through the system block.
|
||||||
assert!(
|
assert!(
|
||||||
|
|||||||
+19
-3
@@ -29,14 +29,30 @@ Each repo can carry two distinct, complementary files:
|
|||||||
"memory",
|
"memory",
|
||||||
"old handoffs"
|
"old handoffs"
|
||||||
],
|
],
|
||||||
|
"protected_invariants": [
|
||||||
|
"do not break old-session transcript replay"
|
||||||
|
],
|
||||||
|
"branch_policy": "PRs target the integration branch, not main",
|
||||||
"verification_policy": {
|
"verification_policy": {
|
||||||
"before_claiming_done": ["run focused tests", "read changed files back"]
|
"before_claiming_done": ["run focused tests", "read changed files back"]
|
||||||
}
|
},
|
||||||
|
"escalate_when": [
|
||||||
|
"a destructive action was not explicitly authorized"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
When present, it is rendered into the system prompt as a higher-authority
|
All fields are optional. When present, the file is rendered into the system
|
||||||
block and takes precedence over a legacy `WHALE.md`.
|
prompt as concise prose in a higher-authority block and takes precedence over
|
||||||
|
a legacy `WHALE.md`.
|
||||||
|
|
||||||
|
This is the **local-law** layer in CodeWhale's hierarchy: *base myth & global
|
||||||
|
Constitution* (the model prompt in `prompts/base.md`, including the Brother
|
||||||
|
Whale identity anchor) → *repo constitution* (`.codewhale/constitution.json`,
|
||||||
|
this file) → *task packet* (the current objective) → *runtime policy*
|
||||||
|
(permissions/sandbox/cost limits enforced in code). The repo constitution
|
||||||
|
gives decision rules; it does not replace the global Constitution or the
|
||||||
|
current user request.
|
||||||
|
|
||||||
> **`WHALE.md` is deprecated.** It overlapped confusingly with `AGENTS.md`.
|
> **`WHALE.md` is deprecated.** It overlapped confusingly with `AGENTS.md`.
|
||||||
> CodeWhale still **reads** an existing `WHALE.md` (below `AGENTS.md`) so old
|
> CodeWhale still **reads** an existing `WHALE.md` (below `AGENTS.md`) so old
|
||||||
|
|||||||
Reference in New Issue
Block a user