Merge origin/main into fix/dedupe-installer-download

Picks up #7108, which restores main's clippy and rustfmt gate (CW-7525).

Signed-off-by: CodeWhale Bot <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
CodeWhale BotandClaude Opus 5.5 committed 2026-10-27 16:08:06 -07:00
commit 35ebbd6bb7
67 files changed
+940 -685

No files matched your search

+1
View File
@@ -63,6 +63,7 @@ crates/tui/extension-host/dist/** text eol=lf
# Reviewed extension-host fixtures carry exact source SHA-256 receipts.
# A Windows checkout must not rewrite their admitted source closure to CRLF.
crates/tui/tests/fixtures/extension_host/** text eol=lf
crates/tui/extension-host/src/dsh/dsh-message-compat.js text eol=lf
# Rustfmt writes LF; keep Rust sources stable across Windows/Linux/macOS.
*.rs text eol=lf
+2
View File
@@ -270,3 +270,5 @@ Henrik Bakke Dukefoss = Henrik Bakke Dukefoss <[email protected]
[email protected] = Henrik Bakke Dukefoss <[email protected]>
[email protected] = Dhruv Singal <[email protected]>
[email protected] = Boyce <[email protected]>
Boyce = Boyce <[email protected]>
+25
View File
@@ -27,6 +27,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **MCP catalogs stay fresh:** a server's tools, resources, prompts and skills are rediscovered on their own connection when an advertised `ttlMs` expires or the server sends a list-changed notice. A tool resolved against the old catalog is refused with "retry the call", a removed tool cannot run, and a failed refresh reconnects instead of serving stale entries. Catalogs are never shared across servers, credentials or reconnects. (CW-7547)
- **MCP Skills extension:** servers that advertise the published Skills extension have their skills listed in `list_mcp_resources`. Listing never activates a skill, and reading a skill file is refused unless its size and sha256 digest match the listing. MCP skills do not yet join `/skills`. (CW-7545)
- **Computer Use is installed, not bundled:** the Computer Use plugin is no longer vendored into the binary. Install it from the `codewhale` marketplace, which points at its independent release; its consent tools load on first use, and decisions still require a human approval card. (CW-6732)
- **First-party marketplace receipt:** the shipped catalog is pinned to marketplace revision `a5f1d210372ff48c9a278a0a39737956ac0f2d93`.
- **Tool image validation:** an image returned by a tool must decode before it is sent, so truncated or corrupt files are omitted with a receipt even when they are already within the delivery budget.
- **Image-heavy session recovery:** ordinary image reads use a 256 KiB delivery budget. Requests measure serialized bytes, omit older images from an outbound copy with receipts, and retry an initial HTTP 413 once only with a smaller body. Saved images and executed tool actions remain intact. (CW-7550)
@@ -209,6 +210,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
is now interrupted instead of running on with nothing able to stop it
(#6558).
- Multiline mode now has a send key on every terminal: with
`composer_multiline_mode = true`, `Enter` inserts a newline and `Alt-Enter`
sends. `Shift-Enter` also sends where the terminal reports it distinctly; on
Linux terminals that report it as plain `Enter`, multiline mode previously
had no way to send. `Ctrl-J` always inserts a newline. Thanks
[@luestr](https://github.com/luestr) for the report. (#7093)
- Session saves no longer fail with "Fleet artifact path must stay within the
workspace" when the Codewhale state root is a symlink or Windows junction to
another volume: the state root is resolved before session-side state is
opened. Thanks [@SparkofSpike](https://github.com/SparkofSpike). (#7097)
- README translations carry refreshed source stamps after the 0.10.3 batch.
Thanks [@Aboysky](https://github.com/Aboysky). (#7104)
### Contributors
- **[@7jrxt42BxFZo4iAnN4CX](https://github.com/7jrxt42BxFZo4iAnN4CX)** — per-scope instruction budgets with visible truncation ([#7086](https://github.com/codewhale-hq/Codewhale/pull/7086), [#6526](https://github.com/codewhale-hq/Codewhale/issues/6526)).
- **[@Lstarsky0](https://github.com/Lstarsky0)** — kept the `/diff` pager's place across a resize ([#7087](https://github.com/codewhale-hq/Codewhale/pull/7087)), said "Goal cleared." only when there was a goal ([#7089](https://github.com/codewhale-hq/Codewhale/pull/7089)), and stopped printing an unpriced session cost as `$0.0000` in `/status` ([#7090](https://github.com/codewhale-hq/Codewhale/pull/7090)).
- **[@gaord](https://github.com/gaord)** — a running turn's cost and context in the Runtime ([#7074](https://github.com/codewhale-hq/Codewhale/pull/7074)).
- **[Sh1Zuku / @SparkofSpike](https://github.com/SparkofSpike)** — resolved a relocated state root before session-side state opens ([#7097](https://github.com/codewhale-hq/Codewhale/pull/7097)).
- **[Boyce / @Aboysky](https://github.com/Aboysky)** — refreshed the README translation source stamps ([#7104](https://github.com/codewhale-hq/Codewhale/pull/7104)).
- **[@M-Maciej](https://github.com/M-Maciej)** — reported answerless output-limit stops in long Fleet proofs ([#6990](https://github.com/codewhale-hq/Codewhale/issues/6990)).
- **[@asto18089](https://github.com/asto18089)** — diagnosed the NoNewPrivs child-matrix failure on hardened containers ([#7021](https://github.com/codewhale-hq/Codewhale/issues/7021)).
- **[@luestr](https://github.com/luestr)** — reported the Linux multiline send key ([#7093](https://github.com/codewhale-hq/Codewhale/issues/7093)) and session export guidance ([#6996](https://github.com/codewhale-hq/Codewhale/issues/6996)).
## [0.10.2] - 2026-10-22
Codewhale v0.10.2 adds an animated pet view (`/pet on`), a live Terminal work
+2 -2
View File
@@ -208,7 +208,7 @@ pub struct ReviewOperation {
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum Op {
Review {
turn: TurnSpec,
turn: Box<TurnSpec>,
request: ReviewOperation,
},
/// Drive one model turn: `role=user` content plus the resolved route
@@ -629,7 +629,7 @@ mod tests {
};
vec![
Op::Review {
turn,
turn: Box::new(turn),
request: ReviewOperation {
version: 1,
repository: "owner/repo".into(),
@@ -49,6 +49,12 @@ impl ModelEnvGuard {
}
}
impl Default for ModelEnvGuard {
fn default() -> Self {
Self::new()
}
}
impl Drop for ModelEnvGuard {
fn drop(&mut self) {
// Safety: test-only environment restoration under the same lock.
+160 -160
View File
@@ -393,6 +393,166 @@ fn error_word_is_allowlisted(word: &str) -> bool {
!looks_opaque(word)
}
const MAX_RECEIPT_FIELD_CHARS: usize = 96;
pub fn safe_receipt_field(raw: &str) -> String {
let sanitized = sanitize_persisted_route_label(raw);
let mut out = String::with_capacity(raw.len().min(MAX_RECEIPT_FIELD_CHARS));
let mut previous_separator = false;
for ch in sanitized.chars() {
if out.chars().count() >= MAX_RECEIPT_FIELD_CHARS {
break;
}
let safe = if ch.is_alphanumeric() || matches!(ch, '.' | '_' | '-' | '/' | ':' | '+') {
ch
} else {
'_'
};
let separator = safe == '_';
if separator && previous_separator {
continue;
}
out.push(safe);
previous_separator = separator;
}
if out.is_empty() { "-".to_string() } else { out }
}
pub fn sanitize_persisted_route_label(raw: &str) -> String {
const MAX_PERSISTED_ROUTE_LABEL_CHARS: usize = 256;
let value = raw.trim();
let lower = value.to_ascii_lowercase();
if value.is_empty() {
return "-".to_string();
}
// URLs are not route labels. Endpoints have a dedicated, validated hash
// field; persisting a URL here risks leaking userinfo, query credentials,
// or fragments through a custom provider/model name.
if value.contains("://") {
return "redacted-url".to_string();
}
let authorization_value = ["bearer ", "basic ", "digest ", "token ", "apikey "]
.iter()
.any(|scheme| lower.starts_with(scheme))
|| lower.contains("authorization:")
|| lower.contains("proxy-authorization:");
if authorization_value {
return "redacted-credential".to_string();
}
// Reject credential assignments regardless of common casing or separator:
// FOO_API_KEY=..., access-token:..., password = ....
for (index, ch) in value.char_indices() {
if !matches!(ch, '=' | ':') {
continue;
}
let name = lower[..index]
.trim()
.trim_matches(|ch: char| matches!(ch, '"' | '\'' | '{' | '[' | ','));
let name = name.rsplit([' ', ',', ';']).next().unwrap_or(name);
let normalized = name.replace('-', "_");
if normalized.ends_with("api_key")
|| normalized.ends_with("token")
|| normalized.ends_with("secret")
|| normalized.ends_with("password")
|| normalized.ends_with("passwd")
{
return "redacted-credential".to_string();
}
}
// Common credential token prefixes. These are intentionally checked at
// word boundaries so model ids containing an incidental "sk" survive.
let credential_prefix = lower
.split(|ch: char| ch.is_whitespace() || matches!(ch, '=' | ':' | ',' | ';' | '"' | '\''))
.filter(|part| !part.is_empty())
.any(|part| {
[
"sk-",
"sk_",
"rk-",
"pk-",
"ghp_",
"gho_",
"ghu_",
"ghs_",
"github_pat_",
"hf_",
"glpat-",
"xoxb-",
"xoxp-",
"xoxa-",
"akia",
"aiza",
"eyj",
]
.iter()
.any(|prefix| part.starts_with(prefix))
});
if credential_prefix {
return "redacted-credential".to_string();
}
let windows_absolute = value.as_bytes().get(1) == Some(&b':')
&& value
.as_bytes()
.get(2)
.is_some_and(|separator| matches!(separator, b'/' | b'\\'));
let contains_local_root = [
"/users/",
"/volumes/",
"/home/",
"/private/",
"\\users\\",
"file://",
"/.ssh/",
"\\.ssh\\",
]
.iter()
.any(|needle| lower.contains(needle));
let looks_like_relative_path = value.contains('\\')
|| lower.starts_with(".ssh/")
|| lower.starts_with(".ssh\\")
|| lower.split('/').any(|segment| {
matches!(
segment,
"." | ".."
| ".ssh"
| ".config"
| "secrets"
| "secret"
| "credentials"
| "credential"
| "relative"
| "workspace"
| "tmp"
)
});
if std::path::Path::new(value).is_absolute()
|| windows_absolute
|| value.starts_with("~/")
|| value.starts_with("./")
|| value.starts_with("../")
|| contains_local_root
|| looks_like_relative_path
{
return "redacted-local-path".to_string();
}
let bounded: String = value
.chars()
.filter(|ch| !ch.is_control())
.take(MAX_PERSISTED_ROUTE_LABEL_CHARS)
.collect();
if bounded.is_empty() {
"-".to_string()
} else {
bounded
}
}
#[cfg(test)]
mod tests {
use super::*;
@@ -569,163 +729,3 @@ mod tests {
assert!(!safe.contains('\n'));
}
}
const MAX_RECEIPT_FIELD_CHARS: usize = 96;
pub fn safe_receipt_field(raw: &str) -> String {
let sanitized = sanitize_persisted_route_label(raw);
let mut out = String::with_capacity(raw.len().min(MAX_RECEIPT_FIELD_CHARS));
let mut previous_separator = false;
for ch in sanitized.chars() {
if out.chars().count() >= MAX_RECEIPT_FIELD_CHARS {
break;
}
let safe = if ch.is_alphanumeric() || matches!(ch, '.' | '_' | '-' | '/' | ':' | '+') {
ch
} else {
'_'
};
let separator = safe == '_';
if separator && previous_separator {
continue;
}
out.push(safe);
previous_separator = separator;
}
if out.is_empty() { "-".to_string() } else { out }
}
pub fn sanitize_persisted_route_label(raw: &str) -> String {
const MAX_PERSISTED_ROUTE_LABEL_CHARS: usize = 256;
let value = raw.trim();
let lower = value.to_ascii_lowercase();
if value.is_empty() {
return "-".to_string();
}
// URLs are not route labels. Endpoints have a dedicated, validated hash
// field; persisting a URL here risks leaking userinfo, query credentials,
// or fragments through a custom provider/model name.
if value.contains("://") {
return "redacted-url".to_string();
}
let authorization_value = ["bearer ", "basic ", "digest ", "token ", "apikey "]
.iter()
.any(|scheme| lower.starts_with(scheme))
|| lower.contains("authorization:")
|| lower.contains("proxy-authorization:");
if authorization_value {
return "redacted-credential".to_string();
}
// Reject credential assignments regardless of common casing or separator:
// FOO_API_KEY=..., access-token:..., password = ....
for (index, ch) in value.char_indices() {
if !matches!(ch, '=' | ':') {
continue;
}
let name = lower[..index]
.trim()
.trim_matches(|ch: char| matches!(ch, '"' | '\'' | '{' | '[' | ','));
let name = name.rsplit([' ', ',', ';']).next().unwrap_or(name);
let normalized = name.replace('-', "_");
if normalized.ends_with("api_key")
|| normalized.ends_with("token")
|| normalized.ends_with("secret")
|| normalized.ends_with("password")
|| normalized.ends_with("passwd")
{
return "redacted-credential".to_string();
}
}
// Common credential token prefixes. These are intentionally checked at
// word boundaries so model ids containing an incidental "sk" survive.
let credential_prefix = lower
.split(|ch: char| ch.is_whitespace() || matches!(ch, '=' | ':' | ',' | ';' | '"' | '\''))
.filter(|part| !part.is_empty())
.any(|part| {
[
"sk-",
"sk_",
"rk-",
"pk-",
"ghp_",
"gho_",
"ghu_",
"ghs_",
"github_pat_",
"hf_",
"glpat-",
"xoxb-",
"xoxp-",
"xoxa-",
"akia",
"aiza",
"eyj",
]
.iter()
.any(|prefix| part.starts_with(prefix))
});
if credential_prefix {
return "redacted-credential".to_string();
}
let windows_absolute = value.as_bytes().get(1) == Some(&b':')
&& value
.as_bytes()
.get(2)
.is_some_and(|separator| matches!(separator, b'/' | b'\\'));
let contains_local_root = [
"/users/",
"/volumes/",
"/home/",
"/private/",
"\\users\\",
"file://",
"/.ssh/",
"\\.ssh\\",
]
.iter()
.any(|needle| lower.contains(needle));
let looks_like_relative_path = value.contains('\\')
|| lower.starts_with(".ssh/")
|| lower.starts_with(".ssh\\")
|| lower.split('/').any(|segment| {
matches!(
segment,
"." | ".."
| ".ssh"
| ".config"
| "secrets"
| "secret"
| "credentials"
| "credential"
| "relative"
| "workspace"
| "tmp"
)
});
if std::path::Path::new(value).is_absolute()
|| windows_absolute
|| value.starts_with("~/")
|| value.starts_with("./")
|| value.starts_with("../")
|| contains_local_root
|| looks_like_relative_path
{
return "redacted-local-path".to_string();
}
let bounded: String = value
.chars()
.filter(|ch| !ch.is_control())
.take(MAX_PERSISTED_ROUTE_LABEL_CHARS)
.collect();
if bounded.is_empty() {
"-".to_string()
} else {
bounded
}
}
+9 -9
View File
@@ -493,6 +493,15 @@ pub fn read_shell_sentinel(path: &Path) -> String {
.to_string()
}
/// Build a syntactically valid, non-secret JWT fixture without embedding a
/// high-entropy token-shaped literal in Git history.
pub fn future_test_jwt(label: &str) -> String {
use base64::Engine as _;
let payload = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(br#"{"exp":9999999999}"#);
format!("test.{payload}.{label}")
}
#[cfg(test)]
mod tests {
use super::*;
@@ -550,12 +559,3 @@ mod tests {
);
}
}
/// Build a syntactically valid, non-secret JWT fixture without embedding a
/// high-entropy token-shaped literal in Git history.
pub fn future_test_jwt(label: &str) -> String {
use base64::Engine as _;
let payload = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(br#"{"exp":9999999999}"#);
format!("test.{payload}.{label}")
}
+1 -1
View File
@@ -425,7 +425,7 @@ pub fn with_test_active_denylist<R>(list: ReadDenylist, callback: impl FnOnce()
*ACTIVE.write().unwrap() = self.0.take();
}
}
let previous = std::mem::replace(&mut *ACTIVE.write().unwrap(), Some(Arc::new(list)));
let previous = (*ACTIVE.write().unwrap()).replace(Arc::new(list));
let _restore = RestoreActive(previous);
callback()
}
+25
View File
@@ -27,6 +27,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **MCP catalogs stay fresh:** a server's tools, resources, prompts and skills are rediscovered on their own connection when an advertised `ttlMs` expires or the server sends a list-changed notice. A tool resolved against the old catalog is refused with "retry the call", a removed tool cannot run, and a failed refresh reconnects instead of serving stale entries. Catalogs are never shared across servers, credentials or reconnects. (CW-7547)
- **MCP Skills extension:** servers that advertise the published Skills extension have their skills listed in `list_mcp_resources`. Listing never activates a skill, and reading a skill file is refused unless its size and sha256 digest match the listing. MCP skills do not yet join `/skills`. (CW-7545)
- **Computer Use is installed, not bundled:** the Computer Use plugin is no longer vendored into the binary. Install it from the `codewhale` marketplace, which points at its independent release; its consent tools load on first use, and decisions still require a human approval card. (CW-6732)
- **First-party marketplace receipt:** the shipped catalog is pinned to marketplace revision `a5f1d210372ff48c9a278a0a39737956ac0f2d93`.
- **Tool image validation:** an image returned by a tool must decode before it is sent, so truncated or corrupt files are omitted with a receipt even when they are already within the delivery budget.
- **Image-heavy session recovery:** ordinary image reads use a 256 KiB delivery budget. Requests measure serialized bytes, omit older images from an outbound copy with receipts, and retry an initial HTTP 413 once only with a smaller body. Saved images and executed tool actions remain intact. (CW-7550)
@@ -209,6 +210,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
is now interrupted instead of running on with nothing able to stop it
(#6558).
- Multiline mode now has a send key on every terminal: with
`composer_multiline_mode = true`, `Enter` inserts a newline and `Alt-Enter`
sends. `Shift-Enter` also sends where the terminal reports it distinctly; on
Linux terminals that report it as plain `Enter`, multiline mode previously
had no way to send. `Ctrl-J` always inserts a newline. Thanks
[@luestr](https://github.com/luestr) for the report. (#7093)
- Session saves no longer fail with "Fleet artifact path must stay within the
workspace" when the Codewhale state root is a symlink or Windows junction to
another volume: the state root is resolved before session-side state is
opened. Thanks [@SparkofSpike](https://github.com/SparkofSpike). (#7097)
- README translations carry refreshed source stamps after the 0.10.3 batch.
Thanks [@Aboysky](https://github.com/Aboysky). (#7104)
### Contributors
- **[@7jrxt42BxFZo4iAnN4CX](https://github.com/7jrxt42BxFZo4iAnN4CX)** — per-scope instruction budgets with visible truncation ([#7086](https://github.com/codewhale-hq/Codewhale/pull/7086), [#6526](https://github.com/codewhale-hq/Codewhale/issues/6526)).
- **[@Lstarsky0](https://github.com/Lstarsky0)** — kept the `/diff` pager's place across a resize ([#7087](https://github.com/codewhale-hq/Codewhale/pull/7087)), said "Goal cleared." only when there was a goal ([#7089](https://github.com/codewhale-hq/Codewhale/pull/7089)), and stopped printing an unpriced session cost as `$0.0000` in `/status` ([#7090](https://github.com/codewhale-hq/Codewhale/pull/7090)).
- **[@gaord](https://github.com/gaord)** — a running turn's cost and context in the Runtime ([#7074](https://github.com/codewhale-hq/Codewhale/pull/7074)).
- **[Sh1Zuku / @SparkofSpike](https://github.com/SparkofSpike)** — resolved a relocated state root before session-side state opens ([#7097](https://github.com/codewhale-hq/Codewhale/pull/7097)).
- **[Boyce / @Aboysky](https://github.com/Aboysky)** — refreshed the README translation source stamps ([#7104](https://github.com/codewhale-hq/Codewhale/pull/7104)).
- **[@M-Maciej](https://github.com/M-Maciej)** — reported answerless output-limit stops in long Fleet proofs ([#6990](https://github.com/codewhale-hq/Codewhale/issues/6990)).
- **[@asto18089](https://github.com/asto18089)** — diagnosed the NoNewPrivs child-matrix failure on hardened containers ([#7021](https://github.com/codewhale-hq/Codewhale/issues/7021)).
- **[@luestr](https://github.com/luestr)** — reported the Linux multiline send key ([#7093](https://github.com/codewhale-hq/Codewhale/issues/7093)) and session export guidance ([#6996](https://github.com/codewhale-hq/Codewhale/issues/6996)).
## [0.10.2] - 2026-10-22
Codewhale v0.10.2 adds an animated pet view (`/pet on`), a live Terminal work
+1 -1
View File
@@ -111,7 +111,7 @@ pub(crate) struct RemoteControlInferencePermit {
}
#[cfg(any(test, all(feature = "test-thread-scoped-state", debug_assertions)))]
#[cfg_attr(windows, allow(dead_code))]
#[cfg_attr(all(not(test), debug_assertions), expect(dead_code))]
pub(crate) async fn acquire_runtime_chat_inference_ownership() -> RuntimeChatInferenceOwnership {
let gate = runtime_chat_inference_gate().write_owned().await;
RuntimeChatInferenceOwnership { _gate: gate }
+1 -1
View File
@@ -1429,7 +1429,7 @@ impl CodewhaleClient {
// sanitizer, dump the offending indices so we can diagnose where
// they came from on the next failure.
if format!("{error:#}").contains("reasoning_content") {
log_thinking_mode_violations(&body);
log_thinking_mode_violations(body);
}
return Err(error);
}
+16 -4
View File
@@ -5,7 +5,10 @@ use codewhale_config::catalog::{
};
use serde::Deserialize;
#[cfg_attr(windows, allow(dead_code))]
#[cfg_attr(
all(not(test), feature = "test-thread-scoped-state", debug_assertions),
expect(dead_code)
)]
pub(crate) const TAGS_PROBE_TIMEOUT: Duration = Duration::from_secs(2);
/// Upper bound on `/api/show` lookups per probe. A developer box can hold
@@ -208,7 +211,10 @@ pub(crate) fn ollama_catalog_delta(
}
}
#[cfg_attr(windows, allow(dead_code))]
#[cfg_attr(
all(not(test), feature = "test-thread-scoped-state", debug_assertions),
expect(dead_code)
)]
pub(crate) fn probe_client() -> anyhow::Result<reqwest::Client> {
// The first-run probe can run before any provider client has installed
// the rustls crypto provider; the shared builder installs it (the bare
@@ -218,7 +224,10 @@ pub(crate) fn probe_client() -> anyhow::Result<reqwest::Client> {
.build()?)
}
#[cfg_attr(windows, allow(dead_code))]
#[cfg_attr(
all(not(test), feature = "test-thread-scoped-state", debug_assertions),
expect(dead_code)
)]
pub(crate) async fn fetch_text(url: &str) -> anyhow::Result<String> {
let response = probe_client()?.get(url).send().await?;
if !response.status().is_success() {
@@ -229,7 +238,10 @@ pub(crate) async fn fetch_text(url: &str) -> anyhow::Result<String> {
.map_err(|reason| anyhow::anyhow!("Ollama catalog response rejected: {reason:?}"))
}
#[cfg_attr(windows, allow(dead_code))]
#[cfg_attr(
all(not(test), feature = "test-thread-scoped-state", debug_assertions),
expect(dead_code)
)]
pub(crate) async fn fetch_tag_profile(origin: &str, tag: &str) -> anyhow::Result<OllamaTagProfile> {
let response = probe_client()?
.post(format!("{origin}/api/show"))
+1 -1
View File
@@ -3802,7 +3802,7 @@ impl Engine {
controls.active = control.clone();
control
};
Box::pin(self.handle_message_operation(turn, Some(request))).await;
Box::pin(self.handle_message_operation(*turn, Some(request))).await;
}
Op::SendMessage(spec) => {
self.admitted_turn_control = {
@@ -1152,7 +1152,7 @@ async fn user_steer_during_parent_answer_still_gets_a_reply_with_healthy_childre
.expect("steering must preempt an indefinitely silent request");
assert_eq!(status, TurnOutcomeStatus::Completed, "{phase}: {error:?}");
assert_eq!(client.calls.load(Ordering::SeqCst), 3, "{phase}");
let requests = client.requests.lock().unwrap();
let requests = client.requests.lock().unwrap().clone();
assert!(requests[1].messages.iter().any(|message| message.content.iter().any(|block| {
matches!(block, ContentBlock::Text { text, .. } if text.contains("Also read state.txt and include its evidence."))
})), "{phase}");
@@ -1179,7 +1179,6 @@ async fn user_steer_during_parent_answer_still_gets_a_reply_with_healthy_childre
requests[0].system, requests[1].system,
"{phase}: pinned system prefix changed"
);
drop(requests);
let mut events = handle.rx_event.write().await;
let events = std::iter::from_fn(|| events.try_recv().ok()).collect::<Vec<_>>();
assert!(events.iter().all(|event| !matches!(event, Event::ToolCallStarted { model_call: Some(call), .. } if call.provider_id == "call-must-not-run")), "{phase}");
+1 -1
View File
@@ -240,7 +240,7 @@ pub struct TurnSpec {
#[derive(Debug)]
pub enum Op {
Review {
turn: TurnSpec,
turn: Box<TurnSpec>,
request: codewhale_protocol::op::ReviewOperation,
},
/// Send a message to the AI
+1 -1
View File
@@ -1103,7 +1103,7 @@ pub fn event_to_protocol(event: &Event, ids: &ProtocolIds) -> wire::EventMsg {
pub fn op_to_protocol(op: &Op) -> wire_op::Op {
match op {
Op::Review { turn, request } => wire_op::Op::Review {
turn: turn_spec_to_wire(turn),
turn: Box::new(turn_spec_to_wire(turn)),
request: request.clone(),
},
Op::SendMessage(spec) => wire_op::Op::SendMessage(turn_spec_to_wire(spec)),
+1 -1
View File
@@ -91,7 +91,7 @@ fn request(
};
let frame = json!({"jsonrpc":"2.0","id":id,"method":"proc/write","params":params});
match parse_host_message(frame, HostTier::Builtin).unwrap() {
HostMessage::Request { request, .. } => request,
HostMessage::Request { request, .. } => *request,
_ => panic!("decoded process write must be a request"),
}
}
+17 -17
View File
@@ -782,7 +782,7 @@ pub enum HostNotification {
pub enum HostMessage {
Request {
id: u64,
request: HostRequest,
request: Box<HostRequest>,
},
Notification(HostNotification),
Response {
@@ -864,64 +864,64 @@ pub fn parse_host_message(value: Value, tier: HostTier) -> Result<HostMessage, P
.map_err(|reason| perr(format!("{method}: {reason}")))?;
HostMessage::Request {
id,
request: HostRequest::Register(register),
request: Box::new(HostRequest::Register(register)),
}
}
("registry/unregister", Some(id)) => HostMessage::Request {
id,
request: HostRequest::Unregister(params(&method, p)?),
request: Box::new(HostRequest::Unregister(params(&method, p)?)),
},
("user-questions/ask", Some(id)) => HostMessage::Request {
id,
request: HostRequest::UserQuestion(params(&method, p)?),
request: Box::new(HostRequest::UserQuestion(params(&method, p)?)),
},
("session/checkpoint", Some(id)) => HostMessage::Request {
id,
request: HostRequest::SessionCheckpoint(params(&method, p)?),
request: Box::new(HostRequest::SessionCheckpoint(params(&method, p)?)),
},
("core/call", Some(id)) => HostMessage::Request {
id,
request: HostRequest::CoreCall(params(&method, p)?),
request: Box::new(HostRequest::CoreCall(params(&method, p)?)),
},
("net/start", Some(id)) => HostMessage::Request {
id,
request: HostRequest::NetStart(params(&method, p)?),
request: Box::new(HostRequest::NetStart(params(&method, p)?)),
},
("net/fetch", Some(id)) => HostMessage::Request {
id,
request: HostRequest::NetFetch(params(&method, p)?),
request: Box::new(HostRequest::NetFetch(params(&method, p)?)),
},
("net/read", Some(id)) => HostMessage::Request {
id,
request: HostRequest::NetRead(params(&method, p)?),
request: Box::new(HostRequest::NetRead(params(&method, p)?)),
},
("net/release", Some(id)) => HostMessage::Request {
id,
request: HostRequest::NetRelease(params(&method, p)?),
request: Box::new(HostRequest::NetRelease(params(&method, p)?)),
},
("net/close", Some(id)) => HostMessage::Request {
id,
request: HostRequest::NetClose(params(&method, p)?),
request: Box::new(HostRequest::NetClose(params(&method, p)?)),
},
("proc/close", Some(id)) => HostMessage::Request {
id,
request: HostRequest::ProcClose(params(&method, p)?),
request: Box::new(HostRequest::ProcClose(params(&method, p)?)),
},
("proc/write", Some(id)) => HostMessage::Request {
id,
request: HostRequest::ProcWrite(params(&method, p)?),
request: Box::new(HostRequest::ProcWrite(params(&method, p)?)),
},
("proc/read", Some(id)) => HostMessage::Request {
id,
request: HostRequest::ProcRead(params(&method, p)?),
request: Box::new(HostRequest::ProcRead(params(&method, p)?)),
},
("exec/redeem", Some(id)) => HostMessage::Request {
id,
request: HostRequest::ExecutionRedeem(params(&method, p)?),
request: Box::new(HostRequest::ExecutionRedeem(params(&method, p)?)),
},
("proc/launch", Some(id)) => HostMessage::Request {
id,
request: HostRequest::ProcLaunch(params(&method, p)?),
request: Box::new(HostRequest::ProcLaunch(params(&method, p)?)),
},
("host/hello", None) => {
let hello: HelloParams = params(&method, p)?;
@@ -974,7 +974,7 @@ impl HostMessage {
#[must_use]
pub fn to_value(&self) -> Value {
match self {
Self::Request { id, request } => match request {
Self::Request { id, request } => match request.as_ref() {
HostRequest::Register(p) => request_value(*id, "registry/register", to_value(p)),
HostRequest::Unregister(p) => {
request_value(*id, "registry/unregister", to_value(p))
@@ -808,11 +808,10 @@ fn production_methods_follow_their_tier_rows_and_reserved_families_stay_builtin_
for tier in HostTier::ALL {
let parsed = parse_host_message(frame.clone(), tier).unwrap();
assert_eq!(parsed.to_value(), frame);
let HostMessage::Request {
request: HostRequest::UserQuestion(params),
..
} = parsed
else {
let HostMessage::Request { request, .. } = parsed else {
panic!("wrong question variant")
};
let HostRequest::UserQuestion(params) = *request else {
panic!("wrong question variant")
};
assert_eq!(params.questions[0].options.len(), count);
+24 -18
View File
@@ -165,6 +165,7 @@ pub(crate) struct HostLaunch {
/// build pins ([`tier::BUILTIN_MODULES`]), which the host bundle embedded
/// from the same build must agree with ([`check_hello_identity`]).
pub builtin_modules: &'static [BuiltinModule],
#[cfg(not(windows))]
pub native_snapshot_roots: Vec<PathBuf>,
/// Exact verified Native LPAC plan; Builtin retains its labelled exception.
#[cfg(windows)]
@@ -551,7 +552,6 @@ pub(crate) fn plan_launch(
memory_cap,
memory: MemoryEnforcement::JobObject,
builtin_modules: tier::BUILTIN_MODULES,
native_snapshot_roots: Vec::new(),
windows: Some(sandbox),
});
}
@@ -569,8 +569,12 @@ pub(crate) fn plan_launch(
bundle,
&snapshot_roots,
);
let mut launch = host_launch(tier, runtime, args, data, memory_cap, wrapped)?;
launch.native_snapshot_roots = snapshot_roots;
let launch = host_launch(tier, runtime, args, data, memory_cap, wrapped)?;
#[cfg(not(windows))]
let launch = HostLaunch {
native_snapshot_roots: snapshot_roots,
..launch
};
Ok(launch)
}
@@ -810,15 +814,15 @@ fn native_readable_paths(
return Err("Native writable data overlaps a runtime or package read grant".into());
}
canonical.push(resolved);
if path.is_file() {
if let (Some(parent), Some(name)) = (path.parent(), path.file_name()) {
canonical.push(
parent
.canonicalize()
.map_err(|error| error.to_string())?
.join(name),
);
}
if path.is_file()
&& let (Some(parent), Some(name)) = (path.parent(), path.file_name())
{
canonical.push(
parent
.canonicalize()
.map_err(|error| error.to_string())?
.join(name),
);
}
}
paths.extend(canonical);
@@ -1095,6 +1099,7 @@ fn host_launch(
memory_cap,
memory: MemoryEnforcement::planned(runtime.kind),
builtin_modules: tier::BUILTIN_MODULES,
#[cfg(not(windows))]
native_snapshot_roots: Vec::new(),
#[cfg(windows)]
windows: None,
@@ -1556,7 +1561,7 @@ pub(crate) struct HostProcess {
/// How the cap is enforced for this process, settled at the handshake.
memory: Arc<std::sync::OnceLock<MemoryEnforcement>>,
pub sandbox: HostSandbox,
#[cfg_attr(windows, expect(dead_code))]
#[cfg(not(windows))]
native_snapshot_roots: Vec<PathBuf>,
tree: Arc<codewhale_runtime::process_tree::ProcessTree>,
#[cfg(windows)]
@@ -1732,7 +1737,7 @@ impl HostProcess {
if !snapshot_roots.iter().any(|grant| canonical.starts_with(grant) && canonical != *grant) {
return Err("Native snapshot store was not granted to this host; restart Codewhale to admit this store".into());
}
let current = native_snapshot_roots(&[authority.state_path.clone()])?;
let current = native_snapshot_roots(std::slice::from_ref(&authority.state_path))?;
if !current.iter().any(|grant| snapshot_roots.contains(grant) && canonical.starts_with(grant)) {
return Err("Native snapshot store changed after host launch".into());
}
@@ -1941,6 +1946,7 @@ impl HostProcess {
memory_cap: launch.memory_cap,
memory,
sandbox: launch.sandbox.clone(),
#[cfg(not(windows))]
native_snapshot_roots: launch.native_snapshot_roots.clone(),
tree,
#[cfg(windows)]
@@ -2396,7 +2402,7 @@ impl Reader {
};
let _ = call.tx.send(result);
}
HostMessage::Request { id, request } => self.start_host_request(id, request)?,
HostMessage::Request { id, request } => self.start_host_request(id, *request)?,
HostMessage::Notification(notification) => match notification {
HostNotification::Hello(hello) => {
if let Some(tx) = self.handshake.lock().expect("handshake lock").hello.take() {
@@ -3012,7 +3018,7 @@ mod tests {
fn request(rig: &Rig, id: u64, plugin: &str) -> Result<(), String> {
rig.reader.handle(HostMessage::Request {
id,
request: register_request(plugin),
request: Box::new(register_request(plugin)),
})
}
@@ -3060,10 +3066,10 @@ mod tests {
rig.reader
.handle(HostMessage::Request {
id: 6,
request: HostRequest::Unregister(protocol::UnregisterParams {
request: Box::new(HostRequest::Unregister(protocol::UnregisterParams {
owner: owner("p"),
handle: 9,
}),
})),
})
.unwrap();
assert_eq!(
+1 -1
View File
@@ -653,7 +653,7 @@ condition = { type = "exit_code", code = 1 }
problem.event.is_none(),
"the override is not one hook's problem: {problem:?}"
);
assert!(problem_summary(&problem).contains("`[hooks]` setting"));
assert!(problem_summary(problem).contains("`[hooks]` setting"));
// Even unvalidated, the accessors refuse the value rather than hand a
// zero budget to the executor.
+59 -55
View File
@@ -1553,6 +1553,48 @@ fn validate_serve_mode_selection(
Ok(http_selected)
}
#[derive(Args, Debug, Clone)]
struct McpAddArgs {
/// Server name
name: String,
/// Command to launch stdio server
#[arg(long, conflicts_with = "url")]
command: Option<String>,
/// URL for streamable HTTP/SSE server
#[arg(long, conflicts_with = "command")]
url: Option<String>,
/// Explicit URL transport override. Use "sse" for legacy SSE endpoints.
#[arg(long, requires = "url")]
transport: Option<String>,
/// Environment variable containing a bearer token for URL-based servers
#[arg(long, requires = "url")]
bearer_token_env_var: Option<String>,
/// Pre-registered OAuth client ID, bound to --oauth-issuer
#[arg(long, requires_all = ["url", "oauth_issuer"])]
oauth_client_id: Option<String>,
#[arg(
long,
requires = "url",
help = "Exact authorization issuer bound to this OAuth client"
)]
oauth_issuer: Option<String>,
#[arg(
long,
requires = "url",
help = "HTTPS URL of a hosted OAuth client metadata document"
)]
oauth_client_metadata_url: Option<String>,
/// Exact OAuth resource pin checked against server discovery
#[arg(long, requires = "url")]
oauth_resource: Option<String>,
/// OAuth scope to request during login. Repeat or comma-separate.
#[arg(long = "scope", requires = "url", value_delimiter = ',')]
scopes: Vec<String>,
/// Arguments for command-based servers
#[arg(long = "arg", allow_hyphen_values = true)]
args: Vec<String>,
}
#[derive(Subcommand, Debug, Clone)]
enum McpCommand {
/// List configured MCP servers
@@ -1577,46 +1619,7 @@ enum McpCommand {
server: Option<String>,
},
/// Add an MCP server entry
Add {
/// Server name
name: String,
/// Command to launch stdio server
#[arg(long, conflicts_with = "url")]
command: Option<String>,
/// URL for streamable HTTP/SSE server
#[arg(long, conflicts_with = "command")]
url: Option<String>,
/// Explicit URL transport override. Use "sse" for legacy SSE endpoints.
#[arg(long, requires = "url")]
transport: Option<String>,
/// Environment variable containing a bearer token for URL-based servers
#[arg(long, requires = "url")]
bearer_token_env_var: Option<String>,
/// Pre-registered OAuth client ID, bound to --oauth-issuer
#[arg(long, requires_all = ["url", "oauth_issuer"])]
oauth_client_id: Option<String>,
#[arg(
long,
requires = "url",
help = "Exact authorization issuer bound to this OAuth client"
)]
oauth_issuer: Option<String>,
#[arg(
long,
requires = "url",
help = "HTTPS URL of a hosted OAuth client metadata document"
)]
oauth_client_metadata_url: Option<String>,
/// Exact OAuth resource pin checked against server discovery
#[arg(long, requires = "url")]
oauth_resource: Option<String>,
/// OAuth scope to request during login. Repeat or comma-separate.
#[arg(long = "scope", requires = "url", value_delimiter = ',')]
scopes: Vec<String>,
/// Arguments for command-based servers
#[arg(long = "arg", allow_hyphen_values = true)]
args: Vec<String>,
},
Add(Box<McpAddArgs>),
/// Authenticate to a URL-based MCP server using OAuth
Login {
/// Server name
@@ -12224,19 +12227,20 @@ async fn run_mcp_command(
}
Ok(())
}
McpCommand::Add {
name,
command,
url,
transport,
bearer_token_env_var,
oauth_client_id,
oauth_issuer,
oauth_client_metadata_url,
oauth_resource,
scopes,
args,
} => {
McpCommand::Add(args) => {
let McpAddArgs {
name,
command,
url,
transport,
bearer_token_env_var,
oauth_client_id,
oauth_issuer,
oauth_client_metadata_url,
oauth_resource,
scopes,
args,
} = *args;
if command.is_none() && url.is_none() {
bail!("Provide either --command or --url for `mcp add`.");
}
@@ -20932,10 +20936,10 @@ mod mcp_add_arg_tests {
let Some(Commands::Mcp { command }) = cli.command else {
panic!("expected mcp command");
};
let McpCommand::Add { args, .. } = command else {
let McpCommand::Add(args) = command else {
panic!("expected mcp add subcommand");
};
assert_eq!(args, vec!["-y".to_string()]);
assert_eq!(args.args, vec!["-y".to_string()]);
}
}
+12 -3
View File
@@ -59,7 +59,10 @@ pub(crate) fn should_adopt_live_local_ollama(
}
/// Resolve the OpenAI-compat Ollama base URL (`…/v1`) from config defaults.
#[cfg_attr(windows, allow(dead_code))]
#[cfg_attr(
all(not(test), feature = "test-thread-scoped-state", debug_assertions),
expect(dead_code)
)]
pub(crate) fn ollama_v1_base_url(config: &Config) -> String {
config
.resolve_provider_pin_identity("ollama")
@@ -69,7 +72,10 @@ pub(crate) fn ollama_v1_base_url(config: &Config) -> String {
.unwrap_or_default()
}
#[cfg_attr(windows, allow(dead_code))]
#[cfg_attr(
all(not(test), feature = "test-thread-scoped-state", debug_assertions),
expect(dead_code)
)]
fn record_ollama_tags_into_lake(
endpoint_v1: &str,
tags: &[String],
@@ -100,7 +106,10 @@ fn record_ollama_tags_into_lake(
/// Probe local Ollama for a live catalog. Prefers native `/api/tags`, falls
/// back to OpenAI-compat `/v1/models`. Returns `None` when nothing useful
/// answered — never invents a tag.
#[cfg_attr(windows, allow(dead_code))]
#[cfg_attr(
all(not(test), feature = "test-thread-scoped-state", debug_assertions),
expect(dead_code)
)]
pub(crate) async fn probe_live_local_ollama_catalog(
config: &Config,
) -> Option<LiveLocalOllamaCatalog> {
+1 -2
View File
@@ -1902,8 +1902,7 @@ fn load_rules_from_dir(
}
/// User-owned global instructions may intentionally resolve through links.
#[cfg(test)]
#[cfg_attr(windows, expect(dead_code))]
#[cfg(all(test, unix))]
fn load_global_context_file(path: &Path) -> Result<String, ProjectContextError> {
load_global_context_file_limited(path, DEFAULT_FILE_MAX_BYTES)
}
+10 -8
View File
@@ -1556,6 +1556,15 @@ pub(crate) fn measure_static_prefix(system: Option<&SystemPrompt>, tools: &[Tool
const GUARD_PASSES_PER_SCOPE: usize = 2;
type GuardInstructionResult = Result<
(
Option<SystemPrompt>,
Option<crate::project_context::InstructionAssembly>,
Vec<crate::project_context::InstructionNotice>,
),
crate::project_context::InstructionRefusal,
>;
fn replace_installed_instruction_block(
prompt: &mut Option<SystemPrompt>,
raw: &crate::project_context::ProjectContext,
@@ -1598,14 +1607,7 @@ pub(crate) fn guard_instruction_prefix(
mut assembly: Option<crate::project_context::InstructionAssembly>,
budgets: crate::project_context::InstructionBudgets,
tools: &[Tool],
) -> Result<
(
Option<SystemPrompt>,
Option<crate::project_context::InstructionAssembly>,
Vec<crate::project_context::InstructionNotice>,
),
crate::project_context::InstructionRefusal,
> {
) -> GuardInstructionResult {
use crate::project_context::{
InstructionAction, InstructionBudgetPolicy, InstructionLimit, InstructionNotice,
InstructionRefusal, InstructionScope,
+189 -189
View File
@@ -524,6 +524,195 @@ pub fn unset(path: &Path, key: &str) -> Result<()> {
})
}
pub(crate) fn set_provider_model_document(
doc: &mut toml_edit::DocumentMut,
identity: &ProviderIdentity,
model: &str,
) -> anyhow::Result<()> {
let mut config =
codewhale_runtime::config::parse_config_base(&doc.to_string()).map_err(|_| {
anyhow::anyhow!("Could not parse destination route identity; contents omitted")
})?;
crate::plugins::providers::apply_startup_providers(&mut config)?;
persistence::set_verified_provider_model_document(doc, &config, identity, model)
}
/// One persistent owner and atomic write for an explicitly saved route.
///
/// Also returns the undo for exactly this write, taken under the config lock,
/// so a caller whose follow-up apply step is rejected can take back only the
/// switch.
pub(crate) fn persist_provider_selection(
config_path: Option<&Path>,
identity: &ProviderIdentity,
model: Option<&str>,
) -> anyhow::Result<(std::path::PathBuf, codewhale_config::ConfigDocumentUndo)> {
let path = persistence::config_toml_path(config_path)?;
let ((), undo) =
codewhale_config::mutate_config_document_undoable_with_migration(&path, |doc, moved| {
persistence::migrate_legacy_route_preferences(&path, doc)?;
let mut config = codewhale_runtime::config::parse_config_after_locked_migration(
&doc.to_string(),
moved,
)
.map_err(|_| anyhow::anyhow!("Could not parse destination route; contents omitted"))?;
crate::plugins::providers::apply_startup_providers(&mut config)?;
config
.verify_provider_identity(identity)
.map_err(anyhow::Error::msg)?;
if let Some(model) = model {
persistence::set_verified_provider_model_document(doc, &config, identity, model)?;
}
persistence::set_document_value(
doc,
&["provider"],
identity.persisted_id().unwrap_or(identity.key.as_str()),
)?;
reconcile_root_model_aliases(doc, &config, identity)
})?;
Ok((path, undo))
}
/// Keep a root `default_text_model` alias from stranding a route switch,
/// without discarding the choice it holds.
///
/// The root alias is the *active* route's fallback: `Config` resolves it only
/// when the selected route has no model of its own. A switch that leaves the
/// incoming route without a leaf therefore hands it an alias naming the route
/// on its way out, and `Config::load` rejects the file the caller just wrote —
/// a committed switch that produces an unloadable config — or, on a
/// pass-through incoming route that loads it, a model the incoming route never
/// chose while the outgoing route comes back on its catalog default.
///
/// When the outgoing route has no leaf and was resolving the alias, relocate
/// that value onto the outgoing route's own canonical leaf, because it is real
/// saved state, and only then clear the alias. Any other alias the incoming
/// route already shadows with its own leaf is inert and stays: deleting a
/// saved choice to satisfy validation of a value nothing resolves is data
/// loss, not a repair. Likewise an unnamed custom route stores its
/// model in the alias itself and has no leaf to receive it. If the incoming
/// route cannot shadow that value, refuse the switch atomically and ask for an
/// explicit destination model instead of saving an unloadable configuration.
///
/// `previous` is the document's configuration before the switch; `incoming` is
/// the identity now selected. Every route writer calls this, so no writer can
/// keep a private rule about which route owns the root alias.
pub(crate) fn reconcile_root_model_aliases(
doc: &mut toml_edit::DocumentMut,
previous: &codewhale_runtime::config::Config,
incoming: &codewhale_runtime::config::ProviderIdentity,
) -> anyhow::Result<()> {
// A freshly admitted id-less root keeps its legacy alias as a receipt.
// Its effective model is written to the canonical leaf created by this
// same-lock migration; the alias is never another route's saved choice.
if incoming.provider == ProviderKind::Custom && incoming.persisted_id().is_none() {
return Ok(());
}
// The outgoing route was resolving the alias as its own model. Move it
// onto that route's leaf even when the incoming route could load it: a
// pass-through incoming route would otherwise inherit a model it never
// chose, and the outgoing route would come back on its catalog default.
if let Some((outgoing, value)) = previous.root_model_alias_owned_by_outgoing(incoming) {
persistence::set_verified_provider_model_document(doc, previous, &outgoing, &value)?;
unset_root_model_aliases(doc)?;
return Ok(());
}
// Below, only `default_text_model` is read. The legacy root `model` key is
// never what blocks a load: `Config::default_model` already refuses to
// route a foreign legacy value to a provider that cannot serve it, and
// `Config::validate` does not consult it, so relocating it would move a
// value nothing is asking about. It is still cleared together with the
// alias that shadows it (`unset_root_model_aliases`).
const ROOT_KEY: &str = "default_text_model";
let Some(value) = doc
.get(ROOT_KEY)
.and_then(toml_edit::Item::as_str)
.map(str::to_owned)
else {
return Ok(());
};
let mut switched = codewhale_runtime::config::parse_config_base(&doc.to_string())
.map_err(|_| anyhow::anyhow!("Could not parse switched route; contents omitted"))?;
crate::plugins::providers::apply_startup_providers(&mut switched)?;
// `Config::validate` is the single authority on what the incoming route can
// serve, so a writer cannot disagree with the loader. Act only when this
// alias is what the loader rejects: a document already broken for an
// unrelated reason is not this writer's to rewrite.
let mut without_alias = switched.clone();
without_alias.default_text_model = None;
if switched
.provider_config_for(incoming)
.and_then(|entry| entry.model.as_deref())
.is_some()
|| switched.validate().is_ok()
|| without_alias.validate().is_err()
{
return Ok(());
}
// An empty or control-bearing alias names no saved model. Nothing to
// relocate, and clearing it loses nothing.
if value.trim().is_empty() || value.chars().any(char::is_control) {
unset_root_model_aliases(doc)?;
return Ok(());
}
// No leaf can hold this value. Keep the only copy of the user's choice
// rather than discard it for a document the incoming route loads as soon as
// it saves a model of its own.
let outgoing = previous.active_provider_identity().ok();
if outgoing.as_ref().is_some_and(|outgoing| {
outgoing != incoming
&& outgoing.provider == ProviderKind::Custom
&& outgoing.persisted_id().is_none()
}) {
// This same-lock canonicalizer copied the root model before moving
// its endpoint. Clear an alias only when that exact outgoing leaf
// proves the choice survives; otherwise retain the refusal.
if outgoing.as_ref().is_some_and(|outgoing| {
previous
.provider_config_for(outgoing)
.and_then(|entry| entry.model.as_deref())
== Some(value.as_str())
}) {
return unset_root_model_aliases(doc);
}
anyhow::bail!(
"Choose a model for the destination provider before switching from a legacy custom connection; the saved configuration was not changed"
);
}
let Some(outgoing) = outgoing.as_ref().filter(|outgoing| *outgoing != incoming) else {
return Ok(());
};
let mut scoped = switched;
scoped
.scope_to_provider_identity(outgoing)
.map_err(anyhow::Error::msg)?;
if scoped
.provider_config_for(outgoing)
.and_then(|entry| entry.model.as_deref())
.is_none()
{
persistence::set_verified_provider_model_document(doc, previous, outgoing, &value)?;
}
// The outgoing route either already saved its own choice or has just
// received this one, so the alias is now a shadowed duplicate that only
// blocks the incoming route.
unset_root_model_aliases(doc)?;
Ok(())
}
/// Clear the root model alias: `default_text_model` and the legacy root
/// `model` that `Config::default_model` falls back to when the former is
/// unset. While `default_text_model` is present the legacy key resolves on no
/// route, so removing only the alias would resurrect a stale legacy model on
/// the incoming route (a pass-through route would then use a model nobody
/// chose for it). When the legacy key *is* the alias, the caller has already
/// moved its value onto the outgoing route's leaf.
fn unset_root_model_aliases(doc: &mut toml_edit::DocumentMut) -> anyhow::Result<()> {
persistence::unset_document_value(doc, &["default_text_model"])?;
persistence::unset_document_value(doc, &["model"])?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
@@ -899,192 +1088,3 @@ model = "Other-X"
Ok(())
}
}
pub(crate) fn set_provider_model_document(
doc: &mut toml_edit::DocumentMut,
identity: &ProviderIdentity,
model: &str,
) -> anyhow::Result<()> {
let mut config =
codewhale_runtime::config::parse_config_base(&doc.to_string()).map_err(|_| {
anyhow::anyhow!("Could not parse destination route identity; contents omitted")
})?;
crate::plugins::providers::apply_startup_providers(&mut config)?;
persistence::set_verified_provider_model_document(doc, &config, identity, model)
}
/// One persistent owner and atomic write for an explicitly saved route.
///
/// Also returns the undo for exactly this write, taken under the config lock,
/// so a caller whose follow-up apply step is rejected can take back only the
/// switch.
pub(crate) fn persist_provider_selection(
config_path: Option<&Path>,
identity: &ProviderIdentity,
model: Option<&str>,
) -> anyhow::Result<(std::path::PathBuf, codewhale_config::ConfigDocumentUndo)> {
let path = persistence::config_toml_path(config_path)?;
let ((), undo) =
codewhale_config::mutate_config_document_undoable_with_migration(&path, |doc, moved| {
persistence::migrate_legacy_route_preferences(&path, doc)?;
let mut config = codewhale_runtime::config::parse_config_after_locked_migration(
&doc.to_string(),
moved,
)
.map_err(|_| anyhow::anyhow!("Could not parse destination route; contents omitted"))?;
crate::plugins::providers::apply_startup_providers(&mut config)?;
config
.verify_provider_identity(identity)
.map_err(anyhow::Error::msg)?;
if let Some(model) = model {
persistence::set_verified_provider_model_document(doc, &config, identity, model)?;
}
persistence::set_document_value(
doc,
&["provider"],
identity.persisted_id().unwrap_or(identity.key.as_str()),
)?;
reconcile_root_model_aliases(doc, &config, identity)
})?;
Ok((path, undo))
}
/// Keep a root `default_text_model` alias from stranding a route switch,
/// without discarding the choice it holds.
///
/// The root alias is the *active* route's fallback: `Config` resolves it only
/// when the selected route has no model of its own. A switch that leaves the
/// incoming route without a leaf therefore hands it an alias naming the route
/// on its way out, and `Config::load` rejects the file the caller just wrote —
/// a committed switch that produces an unloadable config — or, on a
/// pass-through incoming route that loads it, a model the incoming route never
/// chose while the outgoing route comes back on its catalog default.
///
/// When the outgoing route has no leaf and was resolving the alias, relocate
/// that value onto the outgoing route's own canonical leaf, because it is real
/// saved state, and only then clear the alias. Any other alias the incoming
/// route already shadows with its own leaf is inert and stays: deleting a
/// saved choice to satisfy validation of a value nothing resolves is data
/// loss, not a repair. Likewise an unnamed custom route stores its
/// model in the alias itself and has no leaf to receive it. If the incoming
/// route cannot shadow that value, refuse the switch atomically and ask for an
/// explicit destination model instead of saving an unloadable configuration.
///
/// `previous` is the document's configuration before the switch; `incoming` is
/// the identity now selected. Every route writer calls this, so no writer can
/// keep a private rule about which route owns the root alias.
pub(crate) fn reconcile_root_model_aliases(
doc: &mut toml_edit::DocumentMut,
previous: &codewhale_runtime::config::Config,
incoming: &codewhale_runtime::config::ProviderIdentity,
) -> anyhow::Result<()> {
// A freshly admitted id-less root keeps its legacy alias as a receipt.
// Its effective model is written to the canonical leaf created by this
// same-lock migration; the alias is never another route's saved choice.
if incoming.provider == ProviderKind::Custom && incoming.persisted_id().is_none() {
return Ok(());
}
// The outgoing route was resolving the alias as its own model. Move it
// onto that route's leaf even when the incoming route could load it: a
// pass-through incoming route would otherwise inherit a model it never
// chose, and the outgoing route would come back on its catalog default.
if let Some((outgoing, value)) = previous.root_model_alias_owned_by_outgoing(incoming) {
persistence::set_verified_provider_model_document(doc, previous, &outgoing, &value)?;
unset_root_model_aliases(doc)?;
return Ok(());
}
// Below, only `default_text_model` is read. The legacy root `model` key is
// never what blocks a load: `Config::default_model` already refuses to
// route a foreign legacy value to a provider that cannot serve it, and
// `Config::validate` does not consult it, so relocating it would move a
// value nothing is asking about. It is still cleared together with the
// alias that shadows it (`unset_root_model_aliases`).
const ROOT_KEY: &str = "default_text_model";
let Some(value) = doc
.get(ROOT_KEY)
.and_then(toml_edit::Item::as_str)
.map(str::to_owned)
else {
return Ok(());
};
let mut switched = codewhale_runtime::config::parse_config_base(&doc.to_string())
.map_err(|_| anyhow::anyhow!("Could not parse switched route; contents omitted"))?;
crate::plugins::providers::apply_startup_providers(&mut switched)?;
// `Config::validate` is the single authority on what the incoming route can
// serve, so a writer cannot disagree with the loader. Act only when this
// alias is what the loader rejects: a document already broken for an
// unrelated reason is not this writer's to rewrite.
let mut without_alias = switched.clone();
without_alias.default_text_model = None;
if switched
.provider_config_for(incoming)
.and_then(|entry| entry.model.as_deref())
.is_some()
|| switched.validate().is_ok()
|| without_alias.validate().is_err()
{
return Ok(());
}
// An empty or control-bearing alias names no saved model. Nothing to
// relocate, and clearing it loses nothing.
if value.trim().is_empty() || value.chars().any(char::is_control) {
unset_root_model_aliases(doc)?;
return Ok(());
}
// No leaf can hold this value. Keep the only copy of the user's choice
// rather than discard it for a document the incoming route loads as soon as
// it saves a model of its own.
let outgoing = previous.active_provider_identity().ok();
if outgoing.as_ref().is_some_and(|outgoing| {
outgoing != incoming
&& outgoing.provider == ProviderKind::Custom
&& outgoing.persisted_id().is_none()
}) {
// This same-lock canonicalizer copied the root model before moving
// its endpoint. Clear an alias only when that exact outgoing leaf
// proves the choice survives; otherwise retain the refusal.
if outgoing.as_ref().is_some_and(|outgoing| {
previous
.provider_config_for(outgoing)
.and_then(|entry| entry.model.as_deref())
== Some(value.as_str())
}) {
return unset_root_model_aliases(doc);
}
anyhow::bail!(
"Choose a model for the destination provider before switching from a legacy custom connection; the saved configuration was not changed"
);
}
let Some(outgoing) = outgoing.as_ref().filter(|outgoing| *outgoing != incoming) else {
return Ok(());
};
let mut scoped = switched;
scoped
.scope_to_provider_identity(outgoing)
.map_err(anyhow::Error::msg)?;
if scoped
.provider_config_for(outgoing)
.and_then(|entry| entry.model.as_deref())
.is_none()
{
persistence::set_verified_provider_model_document(doc, previous, outgoing, &value)?;
}
// The outgoing route either already saved its own choice or has just
// received this one, so the alias is now a shadowed duplicate that only
// blocks the incoming route.
unset_root_model_aliases(doc)?;
Ok(())
}
/// Clear the root model alias: `default_text_model` and the legacy root
/// `model` that `Config::default_model` falls back to when the former is
/// unset. While `default_text_model` is present the legacy key resolves on no
/// route, so removing only the alias would resurrect a stale legacy model on
/// the incoming route (a pass-through route would then use a model nobody
/// chose for it). When the legacy key *is* the alias, the caller has already
/// moved its value onto the outgoing route's leaf.
fn unset_root_model_aliases(doc: &mut toml_edit::DocumentMut) -> anyhow::Result<()> {
persistence::unset_document_value(doc, &["default_text_model"])?;
persistence::unset_document_value(doc, &["model"])?;
Ok(())
}
+4 -1
View File
@@ -17152,7 +17152,10 @@ impl RuntimeThreadManager {
submission_id: None,
};
let op = match &input_source {
RuntimeTurnInputSource::Review(request) => Op::Review { turn: spec, request: request.clone() },
RuntimeTurnInputSource::Review(request) => Op::Review {
turn: Box::new(spec),
request: request.clone(),
},
_ => Op::SendMessage(spec),
};
+1 -1
View File
@@ -20231,7 +20231,7 @@ fn restart_rebuild_restores_tool_call_identity_from_persisted_items() -> Result<
let turn = manager.store.load_turn(&checkpoint_turn.id)?;
let items = manager.prepared_items_for_turns(std::slice::from_ref(&turn))?;
assert_eq!(
manager.history_from_records(&[turn.clone()], &items)?,
manager.history_from_records(std::slice::from_ref(&turn), &items)?,
expected
);
first.metadata.as_mut().unwrap()["tool_input"] = json!(r#"{"path":"changed"}"#);
+7 -8
View File
@@ -245,17 +245,16 @@ impl UserInputRequest {
));
}
}
if let Some(UserInputIntent::PlanReview { approve, .. }) = &question.intent {
if !labels.contains(approve)
if let Some(UserInputIntent::PlanReview { approve, .. }) = &question.intent
&& (!labels.contains(approve)
|| question
.detail
.as_ref()
.is_none_or(|detail| detail.trim().is_empty())
{
return Err(ToolError::invalid_input(
"Plan review requires detail and an offered approve choice",
));
}
.is_none_or(|detail| detail.trim().is_empty()))
{
return Err(ToolError::invalid_input(
"Plan review requires detail and an offered approve choice",
));
}
}
Ok(())
+1
View File
@@ -16,6 +16,7 @@ mod response_body;
pub use response_body::read_response_body_capped;
/// A writer that counts bytes written without storing them.
#[derive(Default)]
pub struct CountingWriter {
count: usize,
}
+6
View File
@@ -3447,6 +3447,12 @@ restart. Its `[extension_host]` table (`runtime`, `node`, `bun`) and what the
host's sandbox does on each platform are in
[Writing an extension tool](EXTENSIONS.md).
`[extension_host] mcp_backend` (user config only) chooses the MCP client
independently of the `extension_host` flag: `"host"` (default) uses the official
MCP SDK behind the Rust broker; `"rust"` keeps the previous Rust client for one
release. A host refusal never falls back to Rust. See
[MCP connection lifecycle](MCP.md#connection-lifecycle).
Every flag has a row in [`docs/features.toml`](features.toml), the feature
registry; a test fails when a flag and its row disagree.
+13
View File
@@ -40,6 +40,19 @@ changelog or release notes, and this contributor record.
## Contributors by time
<details open>
<summary><strong>Unreleased (0.10.3 candidate)</strong></summary>
**Contributions**
- **[Boyce / Aboysky](https://github.com/Aboysky)** — refreshed the README translation source stamps ([#7104](https://github.com/codewhale-hq/Codewhale/pull/7104)).
- **[7jrxt42BxFZo4iAnN4CX](https://github.com/7jrxt42BxFZo4iAnN4CX)** — per-scope instruction budgets with visible truncation ([#7086](https://github.com/codewhale-hq/Codewhale/pull/7086)).
- **[Lstarsky0](https://github.com/Lstarsky0)** — `/diff` pager resize, goal-clear wording and unpriced `/status` cost ([#7087](https://github.com/codewhale-hq/Codewhale/pull/7087), [#7089](https://github.com/codewhale-hq/Codewhale/pull/7089), [#7090](https://github.com/codewhale-hq/Codewhale/pull/7090)).
- **[gaord](https://github.com/gaord)** — a running turn's cost and context ([#7074](https://github.com/codewhale-hq/Codewhale/pull/7074)).
- **[Sh1Zuku / SparkofSpike](https://github.com/SparkofSpike)** — relocated state root before session-side state ([#7097](https://github.com/codewhale-hq/Codewhale/pull/7097)).
</details>
<details>
<summary><strong>v0.10.2 — terminal, recovery and reliability</strong></summary>
**Contributions**
+3 -3
View File
@@ -17,8 +17,8 @@ Global key chords are not yet user-configurable — tracked for a future release
| `Ctrl-C` | Cancel current turn / dismiss modal / clear a non-empty draft (recoverable — see `Ctrl-Z`) / arm-then-confirm quit |
| `Ctrl-B` | Release the current shell wait so the turn can continue; foreground commands move into `/jobs`, and background tasks keep running. Use `/jobs` or `Bash` with `action: "wait"` to inspect output |
| `Ctrl-D` | Quit (only when the composer is empty) |
| `Tab` | When the composer is empty, cycle TUI mode: Plan → Work → Operate → Plan |
| `Shift+Tab` | Cycle permission posture: Ask → Auto-Review → Full Access. Live regardless of composer contents or whether a turn is running (suppressed only while a modal other than Config is open) |
| `Tab` | When the composer is empty, cycle TUI mode: Plan → Work → Operate → Plan. Works while a turn is running: the change applies at the agent's next step in the same turn, and the status line says so until it does |
| `Shift+Tab` | Cycle permission posture: Ask → Auto-Review → Full Access. Live regardless of composer contents or whether a turn is running (suppressed only while a modal other than Config is open). During a turn the change applies at the agent's next step; a call you already approved runs only if the new posture is equal or broader. Model, thinking level and provider stay locked until the turn ends |
| `Ctrl-T` | Cycle reasoning effort for the active model. Walks the same ladder as `/model` and `/effort` (catalog or documented route dialect). Always-thinking models omit `off`; Grok 4.6 includes `xhigh`. |
| `Ctrl-Shift-T` | Toggle live transcript overlay (sticky-tail auto-scroll) |
| `Ctrl-R` | Open the resume-session picker |
@@ -46,7 +46,7 @@ Editing the message you're about to send.
|-----------------------------|---------------------------------------------------------|
| `Enter` | Send when idle; queue while busy; with an empty composer, send the next queued follow-up now |
| `Shift-Enter` / `Alt-Enter` / `Ctrl-J` | Insert a newline without sending (idle or busy) |
| `Ctrl-Enter` / `Cmd-Enter` | Send into the current turn; send normally when idle (when the terminal reports it distinctly) |
| `Ctrl-Enter` / `Cmd-Enter` | Steer: send into the current turn, closing the in-flight model request at once (reasoning, text or tool call) and continuing the same turn with your message; send normally when idle (when the terminal reports it distinctly). Text already shown is kept; unfinished reasoning and partial tool calls are discarded, never run. A second `Enter` within 500 ms of queueing a message while busy steers too |
| `Ctrl-U` | Clear the whole draft (recoverable — see `Ctrl-Z`) |
| `Ctrl-Z` | Restore the cleared draft (only while the composer is empty) |
| `Ctrl-C` | Clear the whole draft when no turn is running (recoverable — see `Ctrl-Z`) |
+12
View File
@@ -594,6 +594,18 @@ codewhale mcp tools codewhale
## Connection Lifecycle
As a client, Codewhale connects servers through the official MCP SDK (2.4)
behind its Rust broker by default (`[extension_host] mcp_backend = "host"` in
your user config; `"rust"` selects the previous Rust client and stays available
for one release, and a host refusal never falls back to it). Servers that speak
the 2026-07-28 revision are discovered with `server/discover`. Other stdio and
HTTP servers, including ones that answer the discovery probe with an error or
an unrelated result, connect through the legacy `initialize` handshake, and
configured SSE servers keep theirs. A server that rejects sign-in reports its
real 401 with the `/mcp login` hint, and one whose command cannot start says
why. Rust keeps process, network, credential, catalog, approval and
cancellation authority.
Session boot connects in the background: every enabled, allowed server —
user-configured or contributed by an enabled plugin — is started at session
start, concurrently and without holding the first turn. A plugin server passes
+8 -3
View File
@@ -219,9 +219,14 @@ def report_tui_catalog_contract(tui_config_rs: str) -> list[str]:
errors.append(f"retired provider enum consumer: {path.relative_to(ROOT)}")
if re.search(r"KIND_LOOKUP|FROM_KIND_LOOKUP|ProviderKind::from_kind\(", source):
errors.append(f"retired provider ordinal bridge: {path.relative_to(ROOT)}")
for signature in ("pub(crate) fn active_provider_identity", "pub(crate) fn provider_identities", "pub(crate) fn resolve_provider_selection_identity", "pub(crate) fn verify_provider_identity"):
if signature not in tui_config_rs:
errors.append(f"missing canonical config identity boundary: {signature}")
for function_name in (
"active_provider_identity",
"provider_identities",
"resolve_provider_selection_identity",
"verify_provider_identity",
):
if not re.search(rf"\bpub(?:\(crate\))?\s+fn\s+{function_name}\b", tui_config_rs):
errors.append(f"missing canonical config identity boundary: {function_name}")
descriptor_source = read(ROOT / "crates/config/src/descriptors.rs")
if "PROVIDER_COMPATIBILITY" not in descriptor_source or "pub fn compatibility_for_selector" not in descriptor_source:
errors.append("released presentation identities must use the existing generated descriptor owner")
+2 -1
View File
@@ -32,7 +32,8 @@ export default async function FeaturesPage({ params }: { params: Promise<{ local
<Status tone={TONES[feature.status]}>{t[feature.status]}</Status>
{Object.entries(feature.surfaces).map(([surface, state]) => <Status key={surface} tone={TONES[state]}><bdi>{surface.toUpperCase()}</bdi>: {t[state]}</Status>)}
{feature.flag && <code dir="ltr">{feature.flag}</code>}
{feature.since && (BUILD_FACTS.latestPublishedRelease && (compareVersions(feature.since, BUILD_FACTS.latestPublishedRelease.version) ?? 0) > 0
{feature.since === "unreleased" && <Status tone="idle">{getHome(locale).sourceCandidate}</Status>}
{feature.since && feature.since !== "unreleased" && (BUILD_FACTS.latestPublishedRelease && (compareVersions(feature.since, BUILD_FACTS.latestPublishedRelease.version) ?? 0) > 0
? <Status tone="idle">{getHome(locale).sourceCandidate} <bdi>v{feature.since}</bdi></Status>
: <code dir="ltr">v{feature.since}</code>)}
</div>
+69 -63
View File
@@ -35,8 +35,9 @@ const AVAILABILITY_TONES: StatusTone[] = ["ready", "ready", "idle", "idle"];
/**
* The whale-road homepage: the promise, capabilities and reading
* sections sit on paper. The live v2 whale (the desktop app's own Director)
* rests beside the hero, and the page ends in the sea with the install
* command, running into the footer.
* rests on the top edge of a real terminal capture just under the promise,
* and the page ends in the sea with the install command, running into the
* footer.
*
* One memorable thing moves: the whale. It breathes, glances toward the
* pointer, and performs its existing poses. Everything else is still.
@@ -75,7 +76,7 @@ export default async function HomePage({ params }: { params: Promise<{ locale: s
/>
{/* PAPER — the promise, one primary action, the install command, and
the whale resting on the horizon. */}
the whale resting on the terminal below. */}
<section className="home-hero" aria-labelledby="home-title">
<div className="home-hero-inner">
<div className="home-hero-copy">
@@ -105,45 +106,6 @@ export default async function HomePage({ params }: { params: Promise<{ locale: s
</div>
</section>
<div
className="home-release status-line"
data-source-state={sourceIsPublished ? "published release" : "source candidate"}
data-source-state-label={sourceIsPublished ? d.publishedRelease : d.sourceCandidate}
>
<Status tone={publishedRelease ? "ready" : "idle"}>
{publishedRelease
? fill(d.latestRelease, { tag: publishedRelease.tag })
: d.releaseUnavailable}
</Status>
<span>{`${sourceIsPublished ? d.currentSource : d.sourceCandidate} v${sourceVersion}`}</span>
<span>{facts.license ?? "MIT"}</span>
</div>
{showPromo && (
<section className="home-promo" aria-labelledby="home-promo-title">
<a href={PARTNER_PROMO.href} rel="sponsored" className="home-promo-card">
<span className="figure-frame">
<Image
src={PARTNER_PROMO.image.src}
alt={pickText(PARTNER_PROMO.image.alt, locale)}
width={PARTNER_PROMO.image.width}
height={PARTNER_PROMO.image.height}
unoptimized
/>
</span>
<span className="home-promo-text">
<span className="home-promo-title" id="home-promo-title">{PARTNER_PROMO.title}</span>
<span className="home-promo-body">{pickText(PARTNER_PROMO.body, locale)}</span>
<span className="home-promo-body">{pickText(PARTNER_PROMO.disclosure, locale)}</span>
<span className="section-link">
{pickText(PARTNER_PROMO.cta, locale)}
<Icon name="external" />
</span>
</span>
</a>
</section>
)}
<figure className="home-terminal figure">
<div className="figure-frame" dir="ltr">
<TerminalCapture
@@ -153,32 +115,31 @@ export default async function HomePage({ params }: { params: Promise<{ locale: s
/>
</div>
<figcaption className="figure-caption">
{d.shotPreview} · {d.sourceCandidate} v{TERMINAL_SCREENSHOT.version}
{TERMINAL_SCREENSHOT.version === publishedRelease?.version
? `${d.shotPreview} · v${TERMINAL_SCREENSHOT.version}`
: `${d.shotPreview} · ${d.sourceCandidate} v${TERMINAL_SCREENSHOT.version}`}
</figcaption>
</figure>
<div
className="home-proof"
data-source-state={sourceIsPublished ? "published release" : "source candidate"}
data-source-state-label={sourceIsPublished ? d.publishedRelease : d.sourceCandidate}
>
<Status tone={publishedRelease ? "ready" : "idle"}>
{publishedRelease
? fill(d.latestRelease, { tag: publishedRelease.tag })
: d.releaseUnavailable}
</Status>
{!sourceIsPublished && <span>{`${d.sourceCandidate} v${sourceVersion}`}</span>}
<span>{facts.license ?? "MIT"}</span>
<span>{fill(d.proofContributors, { count: facts.contributorCount })}</span>
{facts.toolCount ? <span>{fill(d.proofTools, { count: facts.toolCount })}</span> : null}
{facts.providers.length > 0 && <span>{fill(d.proofProviders, { count: facts.providers.length })}</span>}
</div>
{/* PAPER — models, connections, and the getting-started path. */}
<div className="home-body">
{showHighlights && (
<Section
id="home-release-highlights"
layout="split"
className="home-section"
title={fill(pickText(highlightsPublished ? RELEASE_HIGHLIGHTS.newHeading : RELEASE_HIGHLIGHTS.comingHeading, locale), { version: RELEASE_HIGHLIGHTS.version })}
scope={highlightsPublished ? undefined : pickText(RELEASE_HIGHLIGHTS.comingLede, locale)}
link={<Link href={`/${locale}/changelog`} className="section-link">{pickText(RELEASE_HIGHLIGHTS.changelogLink, locale)}</Link>}
>
<dl className="ruled-list">
{RELEASE_HIGHLIGHTS.items.map((item) => (
<div key={item.title}>
<dt>{item.title}</dt>
<dd>{pickText(item.body, locale)}</dd>
</div>
))}
</dl>
</Section>
)}
<Section
id="home-capabilities"
layout="split"
@@ -220,6 +181,31 @@ export default async function HomePage({ params }: { params: Promise<{ locale: s
</dl>
</Section>
{showPromo && (
<section className="home-section home-promo" aria-labelledby="home-promo-title">
<a href={PARTNER_PROMO.href} rel="sponsored" className="home-promo-card">
<span className="figure-frame">
<Image
src={PARTNER_PROMO.image.src}
alt={pickText(PARTNER_PROMO.image.alt, locale)}
width={PARTNER_PROMO.image.width}
height={PARTNER_PROMO.image.height}
unoptimized
/>
</span>
<span className="home-promo-text">
<span className="home-promo-title" id="home-promo-title">{PARTNER_PROMO.title}</span>
<span className="home-promo-body">{pickText(PARTNER_PROMO.body, locale)}</span>
<span className="home-promo-body">{pickText(PARTNER_PROMO.disclosure, locale)}</span>
<span className="section-link">
{pickText(PARTNER_PROMO.cta, locale)}
<Icon name="external" />
</span>
</span>
</a>
</section>
)}
<Section
id="home-surfaces"
layout="split"
@@ -284,6 +270,26 @@ export default async function HomePage({ params }: { params: Promise<{ locale: s
<p className="section-scope mt-4">{d.availabilityNote}</p>
</Section>
{showHighlights && (
<Section
id="home-release-highlights"
layout="split"
className="home-section"
title={fill(pickText(highlightsPublished ? RELEASE_HIGHLIGHTS.newHeading : RELEASE_HIGHLIGHTS.comingHeading, locale), { version: RELEASE_HIGHLIGHTS.version })}
scope={highlightsPublished ? undefined : pickText(RELEASE_HIGHLIGHTS.comingLede, locale)}
link={<Link href={`/${locale}/changelog`} className="section-link">{pickText(RELEASE_HIGHLIGHTS.changelogLink, locale)}</Link>}
>
<dl className="ruled-list">
{RELEASE_HIGHLIGHTS.items.map((item) => (
<div key={item.title}>
<dt>{item.title}</dt>
<dd>{pickText(item.body, locale)}</dd>
</div>
))}
</dl>
</Section>
)}
{/* COMMUNITY */}
<section className="home-section" aria-labelledby="home-community">
<div className="section-split">
+32 -25
View File
@@ -3,8 +3,9 @@
/* ------------------------------------------------------------------ */
/*
* Paper holds the promise, capabilities and reading sections.
* The live whale stays beside the hero. The page ends in the sea with the
* install command, which runs on into the footer.
* The live whale rests on the top edge of the real terminal capture under
* the hero. The page ends in the sea with the install command, which runs
* on into the footer.
*/
.home { --home-measure: 36rem; }
@@ -12,7 +13,7 @@
/* ---------- paper: the hero ---------- */
.home-hero {
position: relative;
padding-block: clamp(3.5rem, 9vw, 7.5rem) 0;
padding-block: clamp(2.5rem, 6vw, 4.5rem) 0;
background: var(--bg);
}
@@ -28,7 +29,7 @@
.home-hero-copy {
display: grid;
gap: var(--space-5);
padding-bottom: clamp(3rem, 7vw, 5.5rem);
padding-bottom: clamp(2rem, 4vw, 3rem);
min-width: 0;
}
@@ -37,7 +38,7 @@
English; hyphenation lets German compounds break at syllables. */
.home-title {
max-width: 9.5em;
font-size: clamp(2.5rem, 1.4rem + 4.4vw, 5rem);
font-size: clamp(2.5rem, 1.4rem + 3.6vw, 4.5rem);
hyphens: auto;
font-weight: 650;
line-height: 0.98;
@@ -73,7 +74,8 @@
.home-hero-copy .hero-install { max-width: 32rem; margin-top: var(--space-2); }
.home-hero-copy .actions { margin-top: calc(var(--space-2) * -1); }
/* The whale rests on the horizon: its lowest point meets the line. */
/* The whale rests on the terminal: its lowest point meets the frame's top
edge. */
.home-whale {
display: flex;
justify-content: center;
@@ -81,10 +83,10 @@
min-width: 0;
}
.home-whale .whale-live {
--whale-size: clamp(18rem, 40vw, 34rem);
--whale-size: clamp(16rem, 30vw, 26rem);
width: var(--whale-size);
/* The rig fills the middle 72% of the canvas; drop its belly onto the
waterline where the tide begins. */
terminal frame below. */
margin-bottom: calc(var(--whale-size) * -0.26);
position: relative;
z-index: 2;
@@ -159,29 +161,34 @@
background: var(--horizon-light);
}
/* ---------- release and source status ---------- */
.home-release {
/* ---------- the product, then what backs it ---------- */
.home-terminal {
position: relative;
z-index: 1;
width: var(--container);
margin-inline: auto;
}
.home-terminal .figure-frame {
box-shadow: 0 1px 0 var(--line), 0 32px 64px -32px rgb(10 30 51 / 0.5);
}
/* Release, licence and counts read from facts, never typed into copy. */
.home-proof {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: var(--space-2) var(--space-6);
width: var(--container);
margin-inline: auto;
padding-block: var(--space-5);
border-block: 1px solid var(--line);
border-bottom: 1px solid var(--line);
color: var(--muted);
font-size: var(--text-ui);
}
.home-terminal {
width: var(--container);
max-width: 1000px;
margin-inline: auto;
padding-block-start: var(--space-6);
font-variant-numeric: tabular-nums;
}
/* ---------- partner offer ---------- */
.home-promo {
width: var(--container);
max-width: 1000px;
margin-inline: auto;
padding-block-start: var(--space-6);
}
.home-promo { max-width: 1000px; }
.home-promo-card {
display: grid;
gap: var(--space-4) var(--space-6);
@@ -245,7 +252,7 @@
.home-hero-inner { grid-template-columns: minmax(0, 1fr); }
.home-hero-copy { padding-bottom: var(--space-4); }
.home-whale { justify-content: flex-end; }
.home-whale .whale-live { --whale-size: clamp(14rem, 70vw, 20rem); margin-inline: auto; }
.home-whale .whale-live { --whale-size: clamp(11rem, 48vw, 16rem); }
}
@media (forced-colors: active) {
+11
View File
@@ -588,6 +588,17 @@ a.dir-row:hover .dir-action { color: var(--text); }
.code .code-copy:focus-visible { box-shadow: var(--glow-focus); }
.code .code-copy:hover { background: var(--hover); color: var(--text); }
.code-copy[data-copied="true"] { color: var(--live); }
@media (max-width: 30rem) {
.code pre.code-block { padding-right: 3rem; }
.code .code-copy-label {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
}
}
/* ---------- plate ---------- */
/* The install plate: a raised sheet that holds a choice, a command and its
+38 -38
View File
@@ -5,8 +5,8 @@
"sources": [
{
"path": "docs/INSTALL.md",
"sha256": "2653364d1b79ae769390bbd050b2554474128316d73e9a0c7ad931fe47adcd9e",
"content": "# Installing Codewhale\n\n> 阅读简体中文版:[zh_hans/INSTALL.md](zh_hans/INSTALL.md)\n\nCodewhale is an open-source coding agent that runs in your terminal. You give\nit a task (\"fix the failing test\", \"add a CLI flag\"). It reads your repository,\nedits files and runs commands. In the default **Ask** posture it applies file\nedits inside the workspace immediately (and shows you the diff), but asks before\nrunning shell commands, so commit or stash anything you care about first. It\nworks with many model providers. **DeepSeek** is the default.\n\nThe command is `codewhale`. `codew` is a shorter alias for the same program.\n\nThe installation paths were first checked with **v0.10.0** (released 2026-09-22) on a\nfresh **Ubuntu 24.04 x86_64** machine. The dated\n[install receipts](https://github.com/codewhale-hq/Codewhale/blob/37ecdfcc49bc68a9b0d058b97c3946e62c34bd31/docs/install-report/v0.10.0-2026-09-23/RECEIPTS.md) record the commands and output checked at that time. Steps that\ncould not be run on that machine are marked **(untested on this VM: reason)**.\nmacOS, Windows and Android are out of scope, apart from a few notes. A second\npass re-ran the installer, manual-download, archive and npm paths, the no-key\nchecks and zsh completion on **macOS 26.1 (Apple silicon)**; see\n[macOS notes](#macos-notes). Steps that need a model call were not re-run\nthere.\n\nInstall commands that use `latest` resolve to the latest **published** GitHub\nRelease or package. Between releases, `main` may already describe the next\nversion (for example a v0.10.2 source candidate before its tag). A\nprebuilt candidate is available through the official installer only after its\ntag, checksums and release assets exist. Contributors can build `main` now.\n\n## No account needed\n\nCodewhale needs no account. Keys you save with `/provider` or\n`codewhale auth set` stay on your machine; see\n[API keys and providers](#8-api-keys-and-providers). New Codewhale accounts\nare not open, so this guide does not cover account key storage.\n\n## Test and contribute to current source\n\n`latest` selects the latest published release. To test unreleased repairs,\nbuild canonical GitHub `main`, or CNB `main` after verifying its commit matches.\nThe source version number alone does not establish a published package.\n\n```bash\ngit clone --branch main https://github.com/codewhale-hq/CodeWhale.git\ncd CodeWhale\ncargo build --release --locked -p codewhale-cli\ntarget/release/codewhale --version\n```\n\nSee the Cargo section for build prerequisites; use the repository's stable Rust\nselection (declared minimum 1.89). Windows uses `target/release/codewhale.exe`\nand needs MSVC. A development build is separate from a signed release package.\nFor mainland-China downloads and source validation, see [CNB mirror](CNB_MIRROR.md).\nThe [Tencent Lighthouse bootstrap](../scripts/tencent-lighthouse/README.md) is\nbilingual. An npm registry mirror serves the wrapper; native binary downloads\nstill need an available release asset source.\n\n---\n\n## 60-second quickstart (Linux or macOS)\n\n```bash\n# 1. Install. Downloads two checksum-verified binaries into ~/.local/bin (no sudo).\ncurl -fsSL https://codewhale.net/install.sh | sh\n\n# 2. Make sure ~/.local/bin is on your PATH, now and in future terminals.\necho 'export PATH=\"$HOME/.local/bin:$PATH\"' >> ~/.bashrc # zsh: use ~/.zshrc\nexport PATH=\"$HOME/.local/bin:$PATH\"\ncodewhale --version # -> codewhale 0.10.0 (1be1a703b975)\n\n# 3. Give it a DeepSeek API key (from https://platform.deepseek.com/api_keys).\ncodewhale auth set --provider deepseek # prompts for the key; nothing is echoed\ncodewhale auth status --provider deepseek # \"active source: secret store\"\n\n# 4. Run your first task inside a git repository.\ncd ~/your-project\ncodewhale\n```\n\nIn the TUI, type something concrete:\n\n```text\ncreate a Python file primes.py that prints the first 10 primes, run it, and show me the output\n```\n\nCodewhale writes the file, shows you a diff, then asks **APPROVAL: bash\npython3 primes.py – Do you want to proceed?** Press `y` to allow it once. It\nruns the command and reports the output. Press `Ctrl-D` (with an empty input\nbox) to quit. It prints the command to resume the session later.\n\n> **If nothing happens after you send your first message,** you have no key\n> configured. v0.10.0 doesn't warn you in that case. Press **F3**. If DeepSeek\n> shows `missing key`, press Enter, paste the key, then pick a model and\n> confirm.\n\n---\n\n## Contents\n\n1. [Before you start](#1-before-you-start)\n2. [Install: recommended installer](#2-recommended-installer-curl--sh)\n3. [Install: manual download from GitHub Releases](#3-manual-download-from-github-releases)\n4. [Install: npm](#4-npm)\n5. [Install: Cargo / build from source](#5-cargo-and-building-from-source)\n6. [Install: Homebrew (Linux) and Nix](#6-homebrew-on-linux-and-nix)\n7. [Updating and rolling back](#Line truncated
"sha256": "f96e6d85df956beff9703c9c9c8866ca9c2123e05baea20b23edf3522eb13383",
"content": "# Installing Codewhale\n\n> 阅读简体中文版:[zh_hans/INSTALL.md](zh_hans/INSTALL.md)\n\nCodewhale is an open-source coding agent that runs in your terminal. You give\nit a task (\"fix the failing test\", \"add a CLI flag\"). It reads your repository,\nedits files and runs commands. In the default **Ask** posture it applies file\nedits inside the workspace immediately (and shows you the diff), but asks before\nrunning shell commands, so commit or stash anything you care about first. It\nworks with many model providers. **DeepSeek** is the default.\n\nThe command is `codewhale`. `codew` is a shorter alias for the same program.\n\nThe installation paths were first checked with **v0.10.0** (released 2026-09-22) on a\nfresh **Ubuntu 24.04 x86_64** machine. The dated\n[install receipts](https://github.com/codewhale-hq/Codewhale/blob/37ecdfcc49bc68a9b0d058b97c3946e62c34bd31/docs/install-report/v0.10.0-2026-09-23/RECEIPTS.md) record the commands and output checked at that time. Steps that\ncould not be run on that machine are marked **(untested on this VM: reason)**.\nmacOS, Windows and Android are out of scope, apart from a few notes. A second\npass re-ran the installer, manual-download, archive and npm paths, the no-key\nchecks and zsh completion on **macOS 26.1 (Apple silicon)**; see\n[macOS notes](#macos-notes). Steps that need a model call were not re-run\nthere.\n\nInstall commands that use `latest` resolve to the latest **published** GitHub\nRelease or package. Between releases, `main` may already describe the next\nversion (for example a v0.10.2 source candidate before its tag). A\nprebuilt candidate is available through the official installer only after its\ntag, checksums and release assets exist. Contributors can build `main` now.\n\n## No account needed\n\nCodewhale needs no account. Keys you save with `/provider` or\n`codewhale auth set` stay on your machine; see\n[API keys and providers](#8-api-keys-and-providers). New Codewhale accounts\nare not open, so this guide does not cover account key storage.\n\n## Test and contribute to current source\n\n`latest` selects the latest published release. To test unreleased repairs,\nbuild canonical GitHub `main`, or CNB `main` after verifying its commit matches.\nThe source version number alone does not establish a published package.\n\n```bash\ngit clone --branch main https://github.com/codewhale-hq/CodeWhale.git\ncd CodeWhale\ncargo build --release --locked -p codewhale-cli\ntarget/release/codewhale --version\n```\n\nSee the Cargo section for build prerequisites; use the repository's stable Rust\nselection (declared minimum 1.89). Windows uses `target/release/codewhale.exe`\nand needs MSVC. A development build is separate from a signed release package.\nFor mainland-China downloads and source validation, see [CNB mirror](CNB_MIRROR.md).\nThe [Tencent Lighthouse bootstrap](../scripts/tencent-lighthouse/README.md) is\nbilingual. An npm registry mirror serves the wrapper; native binary downloads\nstill need an available release asset source.\n\n---\n\n## 60-second quickstart (Linux or macOS)\n\n```bash\n# 1. Install. Downloads two checksum-verified binaries into ~/.local/bin (no sudo).\ncurl -fsSL https://codewhale.net/install.sh | sh\n\n# 2. Make sure ~/.local/bin is on your PATH, now and in future terminals.\necho 'export PATH=\"$HOME/.local/bin:$PATH\"' >> ~/.bashrc # zsh: use ~/.zshrc\nexport PATH=\"$HOME/.local/bin:$PATH\"\ncodewhale --version # -> codewhale 0.10.0 (1be1a703b975)\n\n# 3. Give it a DeepSeek API key (from https://platform.deepseek.com/api_keys).\ncodewhale auth set --provider deepseek # prompts for the key; nothing is echoed\ncodewhale auth status --provider deepseek # \"active source: secret store\"\n\n# 4. Run your first task inside a git repository.\ncd ~/your-project\ncodewhale\n```\n\nIn the TUI, type something concrete:\n\n```text\ncreate a Python file primes.py that prints the first 10 primes, run it, and show me the output\n```\n\nCodewhale writes the file, shows you a diff, then asks **APPROVAL: bash\npython3 primes.py – Do you want to proceed?** Press `y` to allow it once. It\nruns the command and reports the output. Press `Ctrl-D` (with an empty input\nbox) to quit. It prints the command to resume the session later.\n\n> **If nothing happens after you send your first message,** you have no key\n> configured. v0.10.0 doesn't warn you in that case. Press **F3**. If DeepSeek\n> shows `missing key`, press Enter, paste the key, then pick a model and\n> confirm.\n\n---\n\n## Contents\n\n1. [Before you start](#1-before-you-start)\n2. [Install: recommended installer](#2-recommended-installer-curl--sh)\n3. [Install: manual download from GitHub Releases](#3-manual-download-from-github-releases)\n4. [Install: npm](#4-npm)\n5. [Install: Cargo / build from source](#5-cargo-and-building-from-source)\n6. [Install: Homebrew (Linux) and Nix](#6-homebrew-on-linux-and-nix)\n7. [Updating and rolling back](#Line truncated
}
]
},
@@ -21,8 +21,8 @@
},
{
"path": "docs/KEYBINDINGS.md",
"sha256": "fa8d8f7bfac4231f66e064202bf7cf335177a4a6a2b21b725630cbba22f94793",
"content": "# Keybindings\n\n> 阅读简体中文版:[zh_hans/KEYBINDINGS.md](zh_hans/KEYBINDINGS.md)\n\nThis is the source-of-truth catalog of every keyboard shortcut the TUI recognizes. Bindings are grouped by **context** — the focus or modal state they fire in. A binding listed under \"Composer\" only takes effect when the composer is focused; one under \"Transcript\" only when the transcript has focus; and so on.\n\nGlobal key chords are not yet user-configurable — tracked for a future release (#436, #437). Hotbar slot actions are configurable with `[[hotbar]]` and `/hotbar`; the Hotbar activation chord remains `Alt-1` through `Alt-8`.\n\n## Global (any context)\n\n| Chord | Action |\n|----------------------|---------------------------------------------------------------|\n| `/help`, `F1` or `Ctrl-/` | Toggle the help overlay. `/help` is what the chrome advertises: it is the one route every terminal delivers, where `F1` is eaten by tmux and some emulators and `Ctrl-/` encoding varies |\n| `F2` | Toggle the typed Settings editor |\n| `F3` | Open the provider/model picker (same as `/provider`) |\n| `Ctrl-K` | Open the command palette (slash-command finder) |\n| `Ctrl-C` | Cancel current turn / dismiss modal / clear a non-empty draft (recoverable — see `Ctrl-Z`) / arm-then-confirm quit |\n| `Ctrl-B` | Release the current shell wait so the turn can continue; foreground commands move into `/jobs`, and background tasks keep running. Use `/jobs` or `Bash` with `action: \"wait\"` to inspect output |\n| `Ctrl-D` | Quit (only when the composer is empty) |\n| `Tab` | When the composer is empty, cycle TUI mode: Plan → Work → Operate → Plan |\n| `Shift+Tab` | Cycle permission posture: Ask → Auto-Review → Full Access. Live regardless of composer contents or whether a turn is running (suppressed only while a modal other than Config is open) |\n| `Ctrl-T` | Cycle reasoning effort for the active model. Walks the same ladder as `/model` and `/effort` (catalog or documented route dialect). Always-thinking models omit `off`; Grok 4.6 includes `xhigh`. |\n| `Ctrl-Shift-T` | Toggle live transcript overlay (sticky-tail auto-scroll) |\n| `Ctrl-R` | Open the resume-session picker |\n| `Ctrl-L` | Compact the conversation context (status line shows progress; no-op while a compaction is already running) |\n| `Ctrl-O` | Open the reasoning detail for the selected or current turn, regardless of composer contents |\n| `Ctrl-Alt-O` | Open the whole-turn Turn Inspector, regardless of composer contents |\n| `Alt-V` / `Option-V` (macOS) | Open the details pager for the selected tool/sub-agent card; with nothing selected, the newest visible error, else the first visible card with details, else the most recent one. Click a card first to keep Alt-V on it while newer cards arrive. Terminals that emit the legacy Option-V glyph are also handled |\n| `Ctrl-Shift-E` / `Cmd-Shift-E` | Toggle the file tree in the workbar |\n| `Ctrl-]` or `Ctrl-Tab` | Cycle the pet view while the pet main view is showing; otherwise cycle the work dock: TODO → Agents → Jobs → Background. `Ctrl-]` is portable; `Ctrl-Tab` requires the kitty keyboard protocol. Terminal.app uses it to switch terminal tabs |\n| `Ctrl-Shift-Tab` | Cycle the pet view or work dock backwards where the terminal sends the kitty keyboard chord. Terminal.app uses it to switch terminal tabs |\n| `Alt-G` / `Alt-Shift-G` | Scroll transcript to top / bottom when the composer is empty |\n| `Alt-1`-`Alt-8` | Dispatch Hotbar slots 1-8 when no modal or inline picker is open |\n| `Alt-!` / `Alt-@` / `Alt-#` / `Alt-$` | Select the workbar panel: Tasks / Agents / Context / Files |\n| `Ctrl-Alt-0` | Toggle the workbar off / back to the bottom placement |\n| `Alt-L` | Open the pager for the last message (composer empty) |\n| `Alt-P` / `Alt-A` / `Alt-Y` | Jump to Plan / Work, or request Full Access (`Alt-Y` is the legacy permission channel — Work + Full Access — not a separate mode; it honors a locked approval policy) |\n| `Ctrl-X` (Activity workbar) | Cancel all running background shell jobs |\n| `Esc` | Close topmost modal · cancel slash menu · dismiss toast. During a compact that is serving an in-flight turn, Esc stops the turn (the compact is collateral). |\n\n## Composer\n\nEditing the message you're about to send.\n\n| Chord | Action |\n|-----------------------------|---------------------------------------------------------|\n| `Enter` Line truncated
"sha256": "72a081c68ffba992e80c288141b1b82f5e5a767145f60cdabc71f2f92fba48e9",
"content": "# Keybindings\n\n> 阅读简体中文版:[zh_hans/KEYBINDINGS.md](zh_hans/KEYBINDINGS.md)\n\nThis is the source-of-truth catalog of every keyboard shortcut the TUI recognizes. Bindings are grouped by **context** — the focus or modal state they fire in. A binding listed under \"Composer\" only takes effect when the composer is focused; one under \"Transcript\" only when the transcript has focus; and so on.\n\nGlobal key chords are not yet user-configurable — tracked for a future release (#436, #437). Hotbar slot actions are configurable with `[[hotbar]]` and `/hotbar`; the Hotbar activation chord remains `Alt-1` through `Alt-8`.\n\n## Global (any context)\n\n| Chord | Action |\n|----------------------|---------------------------------------------------------------|\n| `/help`, `F1` or `Ctrl-/` | Toggle the help overlay. `/help` is what the chrome advertises: it is the one route every terminal delivers, where `F1` is eaten by tmux and some emulators and `Ctrl-/` encoding varies |\n| `F2` | Toggle the typed Settings editor |\n| `F3` | Open the provider/model picker (same as `/provider`) |\n| `Ctrl-K` | Open the command palette (slash-command finder) |\n| `Ctrl-C` | Cancel current turn / dismiss modal / clear a non-empty draft (recoverable — see `Ctrl-Z`) / arm-then-confirm quit |\n| `Ctrl-B` | Release the current shell wait so the turn can continue; foreground commands move into `/jobs`, and background tasks keep running. Use `/jobs` or `Bash` with `action: \"wait\"` to inspect output |\n| `Ctrl-D` | Quit (only when the composer is empty) |\n| `Tab` | When the composer is empty, cycle TUI mode: Plan → Work → Operate → Plan. Works while a turn is running: the change applies at the agent's next step in the same turn, and the status line says so until it does |\n| `Shift+Tab` | Cycle permission posture: Ask → Auto-Review → Full Access. Live regardless of composer contents or whether a turn is running (suppressed only while a modal other than Config is open). During a turn the change applies at the agent's next step; a call you already approved runs only if the new posture is equal or broader. Model, thinking level and provider stay locked until the turn ends |\n| `Ctrl-T` | Cycle reasoning effort for the active model. Walks the same ladder as `/model` and `/effort` (catalog or documented route dialect). Always-thinking models omit `off`; Grok 4.6 includes `xhigh`. |\n| `Ctrl-Shift-T` | Toggle live transcript overlay (sticky-tail auto-scroll) |\n| `Ctrl-R` | Open the resume-session picker |\n| `Ctrl-L` | Compact the conversation context (status line shows progress; no-op while a compaction is already running) |\n| `Ctrl-O` | Open the reasoning detail for the selected or current turn, regardless of composer contents |\n| `Ctrl-Alt-O` | Open the whole-turn Turn Inspector, regardless of composer contents |\n| `Alt-V` / `Option-V` (macOS) | Open the details pager for the selected tool/sub-agent card; with nothing selected, the newest visible error, else the first visible card with details, else the most recent one. Click a card first to keep Alt-V on it while newer cards arrive. Terminals that emit the legacy Option-V glyph are also handled |\n| `Ctrl-Shift-E` / `Cmd-Shift-E` | Toggle the file tree in the workbar |\n| `Ctrl-]` or `Ctrl-Tab` | Cycle the pet view while the pet main view is showing; otherwise cycle the work dock: TODO → Agents → Jobs → Background. `Ctrl-]` is portable; `Ctrl-Tab` requires the kitty keyboard protocol. Terminal.app uses it to switch terminal tabs |\n| `Ctrl-Shift-Tab` | Cycle the pet view or work dock backwards where the terminal sends the kitty keyboard chord. Terminal.app uses it to switch terminal tabs |\n| `Alt-G` / `Alt-Shift-G` | Scroll transcript to top / bottom when the composer is empty |\n| `Alt-1`-`Alt-8` | Dispatch Hotbar slots 1-8 when no modal or inline picker is open |\n| `Alt-!` / `Alt-@` / `Alt-#` / `Alt-$` | Select the workbar panel: Tasks / Agents / Context / Files |\n| `Ctrl-Alt-0` | Toggle the workbar off / back to the bottom placement |\n| `Alt-L` | Open the pager for the last message (composer empty) |\n| `Alt-P` / `Alt-A` / `Alt-Y` | Jump to Plan / Work, or request Full Access (`Alt-Y` is the legacy permission channel — Work + Full Access — not a separate mode; it honors a locked approval policy) |\n| `Ctrl-X` (Activity workbar) | Cancel all running background shell jobs |\n| `Esc` | Close topmost modal · cancel slash menu · dismiss toast. During a compact Line truncated
}
]
},
@@ -32,8 +32,8 @@
"sources": [
{
"path": "docs/CONFIGURATION.md",
"sha256": "3e076746af15d2c2f25bd60d86648c95ca17fc98ec0d9b1bd4d3a025958dd5f3",
"content": "# Configuration\n\ncodewhale reads configuration from a TOML file plus environment variables.\nAt process startup it may also load literal built-in-provider credentials from\na workspace-local `.env` file. Use the tracked `.env.example` as the template;\ncopy it to `.env`, then add only credential values.\n\nA workspace is not configuration authority. Codewhale therefore ignores\nconfig/profile/home paths, provider/model/base-URL routing, MCP/plugin state,\napproval/sandbox/shell posture, executable paths, runtime settings, and every\nother non-credential `.env` entry. Variable expansion is rejected so a\nrepository cannot substitute an ambient secret into a credential value. Use\n`config.toml`, CLI flags, or values exported by the launching shell for those\nexplicit control-plane settings. `.env` is read from a stable regular-file\nhandle, is capped at 1 MiB, and symbolic links, reparse points, and multiply\nlinked files are rejected.\n\n## Reading and checking configuration from the CLI\n\n`codewhale config get <key>` reads scalar keys, whole tables such as `tools`,\nand nested paths such as `tools.user_input_timeout_seconds`. Displayed tables\nand nested values apply the same recursive credential redaction as `config dump`.\n\n`config set` supports its named scalar keys and the provider, route, and\nnotification commands. It refuses a key that nothing reads and suggests the\nnearest real key (`config set calm_mod on` names `calm_mode`). A settings.toml\nkey such as `calm_mode` or `tool_collapse` is validated and written to the\nuser-global settings.toml, not config.toml; `config get` reads it back from\nthere even when an old config.toml copy (which nothing reads) is still present,\nand names that copy so you can `config unset` it. Settings keys have no project\nscope, so `--project` refuses them. A config.toml key is checked against the\ntype its reader expects and stored as a TOML boolean or number where the\nreader needs one (`yolo = true`, `max_subagents = 4`); `reasoning_effort`\naccepts the same aliases as `/effort` (#6563). Other dotted writes fail before\nmodifying the file and name the TOML table to edit. For example, set a tools\ntimeout in the file as:\n\n```toml\n[tools]\nuser_input_timeout_seconds = 0\n```\n\n`codewhale config doctor` checks credential presence and endpoint shape, and\nwarns about each config.toml root key that nothing reads: a settings.toml key\nleft in config.toml is named as misplaced, and any other unread key gets a\ndid-you-mean. Keys read by any runtime reader are not reported. The warnings do\nnot fail the check, and a clean result does not validate every runtime setting's\nvalue (#6083, #6563).\n\n## Constitution, project instructions, and repo authority\n\nCodewhale has several instruction surfaces. They are deliberately separate so a\npersonal constitution, repo policy, project instructions, and runtime security\ncontrols do not blur together.\n\n- **Bundled global Constitution** — the compiled base law in the binary. It is\n the default floor for every session.\n- **User-global constitution** — the normal guided setup output. Manage it with\n `/constitution` or `/setup`; Codewhale stores structured data at\n `$CODEWHALE_HOME/constitution.json` (default `~/.codewhale/constitution.json`)\n and renders it into a separate `<codewhale_user_constitution>` prose block.\n This can express preferences and stop conditions, but it does not change\n runtime approval policy, sandbox, shell, network, trust, or MCP permissions.\n- **Repo-local constitution** — optional project policy in\n `.codewhale/constitution.json`, described below.\n- **`AGENTS.md`** — cross-agent **project instructions** (prose). This is the\n canonical file for \"how should an agent work in this repo.\" Run `/init` to\n scaffold one. Instruction files written for other tools (`CLAUDE.md`,\n `.claude/instructions.md`, `.cursorrules`, `.github/copilot-instructions.md`\n and similar) are read only when you opt in with the top-level\n `project_instruction_imports` key, for example `[\"claude\"]`, `[\"cursor\"]` or\n `[\"all\"]` (see `config.example.toml`); Codewhale warns when it finds one it\n did not load.\n\n Codewhale reads one instruction file per directory, from the repository\n root down to the workspace, and puts your global instruction file (such as\n `~/.codewhale/AGENTS.md`) in front of them. The nearest scope reads last\n and wins: the workspace file overrides its parents, and project files\n override the global file. Global and project instructions are budgeted\n separately in estimated tokens (see [Instruction budget](#instruction-budget-instruction_budget-6526)\n below): a scope over its budget drops whole files from the broadest end\n first, and a single file larger than the remaining budget keeps its head\n with a marker that names the bytes and tokens dropped. A file keeps at most\n its first 100 KiB, and a file larger than `file_max_bytesLine truncated
"sha256": "c36db36c85ad3be7d11a01dbdf0c74a5641844a62169431db5988a127a34cd69",
"content": "# Configuration\n\ncodewhale reads configuration from a TOML file plus environment variables.\nAt process startup it may also load literal built-in-provider credentials from\na workspace-local `.env` file. Use the tracked `.env.example` as the template;\ncopy it to `.env`, then add only credential values.\n\nA workspace is not configuration authority. Codewhale therefore ignores\nconfig/profile/home paths, provider/model/base-URL routing, MCP/plugin state,\napproval/sandbox/shell posture, executable paths, runtime settings, and every\nother non-credential `.env` entry. Variable expansion is rejected so a\nrepository cannot substitute an ambient secret into a credential value. Use\n`config.toml`, CLI flags, or values exported by the launching shell for those\nexplicit control-plane settings. `.env` is read from a stable regular-file\nhandle, is capped at 1 MiB, and symbolic links, reparse points, and multiply\nlinked files are rejected.\n\n## Reading and checking configuration from the CLI\n\n`codewhale config get <key>` reads scalar keys, whole tables such as `tools`,\nand nested paths such as `tools.user_input_timeout_seconds`. Displayed tables\nand nested values apply the same recursive credential redaction as `config dump`.\n\n`config set` supports its named scalar keys and the provider, route, and\nnotification commands. It refuses a key that nothing reads and suggests the\nnearest real key (`config set calm_mod on` names `calm_mode`). A settings.toml\nkey such as `calm_mode` or `tool_collapse` is validated and written to the\nuser-global settings.toml, not config.toml; `config get` reads it back from\nthere even when an old config.toml copy (which nothing reads) is still present,\nand names that copy so you can `config unset` it. Settings keys have no project\nscope, so `--project` refuses them. A config.toml key is checked against the\ntype its reader expects and stored as a TOML boolean or number where the\nreader needs one (`yolo = true`, `max_subagents = 4`); `reasoning_effort`\naccepts the same aliases as `/effort` (#6563). Other dotted writes fail before\nmodifying the file and name the TOML table to edit. For example, set a tools\ntimeout in the file as:\n\n```toml\n[tools]\nuser_input_timeout_seconds = 0\n```\n\n`codewhale config doctor` checks credential presence and endpoint shape, and\nwarns about each config.toml root key that nothing reads: a settings.toml key\nleft in config.toml is named as misplaced, and any other unread key gets a\ndid-you-mean. Keys read by any runtime reader are not reported. The warnings do\nnot fail the check, and a clean result does not validate every runtime setting's\nvalue (#6083, #6563).\n\n## Constitution, project instructions, and repo authority\n\nCodewhale has several instruction surfaces. They are deliberately separate so a\npersonal constitution, repo policy, project instructions, and runtime security\ncontrols do not blur together.\n\n- **Bundled global Constitution** — the compiled base law in the binary. It is\n the default floor for every session.\n- **User-global constitution** — the normal guided setup output. Manage it with\n `/constitution` or `/setup`; Codewhale stores structured data at\n `$CODEWHALE_HOME/constitution.json` (default `~/.codewhale/constitution.json`)\n and renders it into a separate `<codewhale_user_constitution>` prose block.\n This can express preferences and stop conditions, but it does not change\n runtime approval policy, sandbox, shell, network, trust, or MCP permissions.\n- **Repo-local constitution** — optional project policy in\n `.codewhale/constitution.json`, described below.\n- **`AGENTS.md`** — cross-agent **project instructions** (prose). This is the\n canonical file for \"how should an agent work in this repo.\" Run `/init` to\n scaffold one. Instruction files written for other tools (`CLAUDE.md`,\n `.claude/instructions.md`, `.cursorrules`, `.github/copilot-instructions.md`\n and similar) are read only when you opt in with the top-level\n `project_instruction_imports` key, for example `[\"claude\"]`, `[\"cursor\"]` or\n `[\"all\"]` (see `config.example.toml`); Codewhale warns when it finds one it\n did not load.\n\n Codewhale reads one instruction file per directory, from the repository\n root down to the workspace, and puts your global instruction file (such as\n `~/.codewhale/AGENTS.md`) in front of them. The nearest scope reads last\n and wins: the workspace file overrides its parents, and project files\n override the global file. Global and project instructions are budgeted\n separately in estimated tokens (see [Instruction budget](#instruction-budget-instruction_budget-6526)\n below): a scope over its budget drops whole files from the broadest end\n first, and a single file larger than the remaining budget keeps its head\n with a marker that names the bytes and tokens dropped. A file keeps at most\n its first 100 KiB, and a file larger than `file_max_bytesLine truncated
},
{
"path": "docs/CODEWHALE_AGENT.md",
@@ -48,8 +48,8 @@
"sources": [
{
"path": "docs/PROVIDERS.md",
"sha256": "a1b48aeee2b2f2aadc60b1c1ee8646cef8733c5f9a4c1f8087e958fa53123ffd",
"content": "# Provider Registry\n\n> 阅读简体中文版:[zh_hans/PROVIDERS.md](zh_hans/PROVIDERS.md)\n\nSpeech recognition, synthesis, translation and video have separate execution\npaths. See [media models](MEDIA_MODELS.md) for current support and reviewed\nupstream candidates; a chat catalogue row alone does not add a media backend.\n\nThis registry describes provider behavior that is wired into the current\nCodewhale codebase. It is intentionally conservative: shipped entries are\nlimited to provider IDs, config keys, auth paths, base URLs, model resolution,\nand capability metadata that the code already knows about.\n\nThe `/provider` and `/model` pickers recommend OrcaRouter as a partner option.\nClick the recommendation or press F7 to view the Codewhale × OrcaRouter offer:\na **30% top-up match for DeepSeek V4.1 Flash**. The pager includes the\n[redemption link](https://www.orcarouter.ai/redeem/1a6250545fdc4e0aaccb0a4470159e67)\nand can copy its text. Codewhale earns a referral fee from OrcaRouter.\nCurrent eligibility and terms are owned by the linked offer page. Viewing it\npreserves the current provider, model and credentials; redemption happens on\nOrcaRouter's site.\n\nDeepSeek remains the default provider, but every entry in `ProviderKind::ALL`\nis a first-class selectable provider route. `ALL` is the catalog/picker\nsurface — one identity per vendor. Dual-wire dialect kinds (`*Anthropic`, e.g.\n`deepseek-anthropic`) and the Model Studio plan variants stay on the enum for\nserde and `provider_for_kind` but are deliberately **not** catalog rows:\na plan is `mode`/`base_url` and a dialect is `wire = openai|anthropic` on the\nprimary provider config (`crates/config/src/provider_kind.rs:221-226`). Hosted\nroutes, generic OpenAI-compatible endpoints, the OpenAI Codex/ChatGPT route,\nnative Anthropic, and local runtimes all run the same terminal harness against\nthe selected provider/model/base URL.\n\nA host reached over plain Chat Completions is an ordinary named provider,\nnot a `ProviderKind`: enum variants are reserved for distinct *wires*\n(Anthropic Messages, Codex Responses, Google thought signatures). Any such\nhost is a `[providers.<name>]` table with a base URL, a model, and a key env\n(`docs/CONFIGURATION.md`); `/provider` and `/setup` keep a \"paste a Base URL\nand a key\" path for exactly this. Offerings come from live `GET /v1/models`\nplus the Codewhale catalog rather than a compiled roster (#5350, #6289).\n\nKnown-good hosts. These ship as bundled descriptor rows in\n`crates/config/assets/provider_descriptors.json` (compiled in by\n`crates/config/src/descriptors.rs`): each row says how to reach the host — wire,\nbase URL, key env, aliases — while model ids stay live from `GET /v1/models`\nand the Codewhale catalog; the example model is only a bootstrap hint. Verify\nagainst the vendor's own docs before trusting any value here:\n\n| Host | Base URL | Example models | API key env |\n| --- | --- | --- | --- |\n| SenseNova | `https://token.sensenova.cn/v1` | `deepseek-v4-flash` | `SENSENOVA_API_KEY` |\n| Baseten | `https://inference.baseten.co/v1` | `deepseek-ai/DeepSeek-V4-Pro` | `BASETEN_API_KEY` |\n| Groq | `https://api.groq.com/openai/v1` | `llama-3.3-70b-versatile` | `GROQ_API_KEY` |\n| Cerebras | `https://api.cerebras.ai/v1` | `llama-3.3-70b` | `CEREBRAS_API_KEY` |\n| Command Code | `https://api.commandcode.ai/provider/v1` | `deepseek/deepseek-v4-flash` | `COMMAND_CODE_API_KEY` |\n| Alibaba Model Studio (DashScope) | `https://dashscope-intl.aliyuncs.com/compatible-mode/v1` | `qwen3.8-flash` | `DASHSCOPE_API_KEY` |\n| AICraft | `https://aicraftapi.com/v1` | `claude-4.6-sonnet`; DeepSeek / Claude / Gemini / Qwen / GLM / MiniMax / Doubao families | `AICRAFT_API_KEY` |\n| Tsubasa | `https://api.tsubasa.sh/v1` | `tsubasa-pro`, `tsubasa-fast` (32,768-token context) | `TSUBASA_API_KEY` |\n| Cheaper Inference | `https://api.cheaperinference.com/v1` | `gpt-5.4-mini`, `claude-sonnet-5`, `gemini-3.1-pro` | `CHEAPER_INFERENCE_API_KEY` |\n| Yolo-Auto | `https://yolo-auto.com/v1` | `qwen3.8-flash`; `yolo` / `yolo-small` | `YOLO_AUTO_API_KEY` |\n| Standard Compute | `https://api.stdcmpt.com/v1` | `standardcompute` (router); `deepseek/deepseek-v4-pro`, `anthropic/claude-sonnet-5.5` | `STANDARDCOMPUTE_API_KEY` |\n\nAICraft's roster spans DeepSeek, Anthropic Claude, Google Gemini, Qwen, GLM,\nMiniMax and Doubao ids on its OpenAI-compatible endpoint. The authority is\n`GET https://aicraftapi.com/v1/models` with your key — pick a model from that\nlist, not from this table.\nTsubasa implements only `GET /v1/models` and Chat Completions. Its two public\nmodel ids share a 32,768-token context, smaller than the 128,000 tokens Codewhale assumes\nfor an unknown model, so set it on the route after saving:\n`codewhale config set providers.tsubasa.context_window 32768`.\n\nCheaper Inference is an OpenAI-compatible gateway with one key for models from\nseveral labs. Model ids are bare, sucLine truncated
"sha256": "39efe1ac88aaf5752a54c9267ffd9f26066bd4b46984713602d8bd14adb46508",
"content": "# Provider Registry\n\n> 阅读简体中文版:[zh_hans/PROVIDERS.md](zh_hans/PROVIDERS.md)\n\nSpeech recognition, synthesis, translation and video have separate execution\npaths. See [media models](MEDIA_MODELS.md) for current support and reviewed\nupstream candidates; a chat catalogue row alone does not add a media backend.\n\nThis registry describes provider behavior that is wired into the current\nCodewhale codebase. It is intentionally conservative: shipped entries are\nlimited to provider IDs, config keys, auth paths, base URLs, model resolution,\nand capability metadata that the code already knows about.\n\nThe `/provider` and `/model` pickers recommend OrcaRouter as a partner option.\nClick the recommendation or press F7 to view the Codewhale × OrcaRouter offer:\na **30% top-up match for DeepSeek V4.1 Flash**. The pager includes the\n[redemption link](https://www.orcarouter.ai/redeem/1a6250545fdc4e0aaccb0a4470159e67)\nand can copy its text. Codewhale earns a referral fee from OrcaRouter.\nCurrent eligibility and terms are owned by the linked offer page. Viewing it\npreserves the current provider, model and credentials; redemption happens on\nOrcaRouter's site.\n\nDeepSeek remains the default provider, but every entry in `ProviderKind::ALL`\nis a first-class selectable provider route. `ALL` is the catalog/picker\nsurface — one identity per vendor. Dual-wire dialect kinds (`*Anthropic`, e.g.\n`deepseek-anthropic`) and the Model Studio plan variants stay on the enum for\nserde and `provider_for_kind` but are deliberately **not** catalog rows:\na plan is `mode`/`base_url` and a dialect is `wire = openai|anthropic` on the\nprimary provider config (`crates/config/src/provider_kind.rs:221-226`). Hosted\nroutes, generic OpenAI-compatible endpoints, the OpenAI Codex/ChatGPT route,\nnative Anthropic, and local runtimes all run the same terminal harness against\nthe selected provider/model/base URL.\n\nA host reached over plain Chat Completions is an ordinary named provider,\nnot a `ProviderKind`: enum variants are reserved for distinct *wires*\n(Anthropic Messages, Codex Responses, Google thought signatures). Any such\nhost is a `[providers.<name>]` table with a base URL, a model, and a key env\n(`docs/CONFIGURATION.md`); `/provider` and `/setup` keep a \"paste a Base URL\nand a key\" path for exactly this. Offerings come from live `GET /v1/models`\nplus the Codewhale catalog rather than a compiled roster (#5350, #6289).\n\nKnown-good hosts. These ship as bundled descriptor rows in\n`crates/config/assets/provider_descriptors.json` (compiled in by\n`crates/config/src/descriptors.rs`): each row says how to reach the host — wire,\nbase URL, key env, aliases — while model ids stay live from `GET /v1/models`\nand the Codewhale catalog; the example model is only a bootstrap hint. Verify\nagainst the vendor's own docs before trusting any value here:\n\n| Host | Base URL | Example models | API key env |\n| --- | --- | --- | --- |\n| SenseNova | `https://token.sensenova.cn/v1` | `deepseek-v4-flash` | `SENSENOVA_API_KEY` |\n| Baseten | `https://inference.baseten.co/v1` | `deepseek-ai/DeepSeek-V4-Pro` | `BASETEN_API_KEY` |\n| Groq | `https://api.groq.com/openai/v1` | `llama-3.3-70b-versatile` | `GROQ_API_KEY` |\n| Cerebras | `https://api.cerebras.ai/v1` | `llama-3.3-70b` | `CEREBRAS_API_KEY` |\n| Command Code | `https://api.commandcode.ai/provider/v1` | `deepseek/deepseek-v4-flash` | `COMMAND_CODE_API_KEY` |\n| Alibaba Model Studio (DashScope) | `https://dashscope-intl.aliyuncs.com/compatible-mode/v1` | `qwen3.8-flash` | `DASHSCOPE_API_KEY` |\n| AICraft | `https://aicraftapi.com/v1` | `claude-4.6-sonnet`; DeepSeek / Claude / Gemini / Qwen / GLM / MiniMax / Doubao families | `AICRAFT_API_KEY` |\n| Tsubasa | `https://api.tsubasa.sh/v1` | `tsubasa-pro`, `tsubasa-fast` (32,768-token context) | `TSUBASA_API_KEY` |\n| Cheaper Inference | `https://api.cheaperinference.com/v1` | `gpt-5.4-mini`, `claude-sonnet-5`, `gemini-3.1-pro` | `CHEAPER_INFERENCE_API_KEY` |\n| Yolo-Auto | `https://yolo-auto.com/v1` | `qwen3.8-flash`; `yolo` / `yolo-small` | `YOLO_AUTO_API_KEY` |\n| Standard Compute | `https://api.stdcmpt.com/v1` | `standardcompute` (router); `deepseek/deepseek-v4-pro`, `anthropic/claude-sonnet-5.5` | `STANDARDCOMPUTE_API_KEY` |\n\nAICraft's roster spans DeepSeek, Anthropic Claude, Google Gemini, Qwen, GLM,\nMiniMax and Doubao ids on its OpenAI-compatible endpoint. The authority is\n`GET https://aicraftapi.com/v1/models` with your key — pick a model from that\nlist, not from this table.\nTsubasa implements only `GET /v1/models` and Chat Completions. Its two public\nmodel ids share a 32,768-token context, smaller than the 128,000 tokens Codewhale assumes\nfor an unknown model, so set it on the route after saving:\n`codewhale config set providers.tsubasa.context_window 32768`.\n\nCheaper Inference is an OpenAI-compatible gateway with one key for models from\nseveral labs. Model ids are bare, sucLine truncated
},
{
"path": "docs/MODEL_LAB.md",
@@ -64,13 +64,13 @@
"sources": [
{
"path": "docs/CONFIGURATION.md",
"sha256": "3e076746af15d2c2f25bd60d86648c95ca17fc98ec0d9b1bd4d3a025958dd5f3",
"content": "# Configuration\n\ncodewhale reads configuration from a TOML file plus environment variables.\nAt process startup it may also load literal built-in-provider credentials from\na workspace-local `.env` file. Use the tracked `.env.example` as the template;\ncopy it to `.env`, then add only credential values.\n\nA workspace is not configuration authority. Codewhale therefore ignores\nconfig/profile/home paths, provider/model/base-URL routing, MCP/plugin state,\napproval/sandbox/shell posture, executable paths, runtime settings, and every\nother non-credential `.env` entry. Variable expansion is rejected so a\nrepository cannot substitute an ambient secret into a credential value. Use\n`config.toml`, CLI flags, or values exported by the launching shell for those\nexplicit control-plane settings. `.env` is read from a stable regular-file\nhandle, is capped at 1 MiB, and symbolic links, reparse points, and multiply\nlinked files are rejected.\n\n## Reading and checking configuration from the CLI\n\n`codewhale config get <key>` reads scalar keys, whole tables such as `tools`,\nand nested paths such as `tools.user_input_timeout_seconds`. Displayed tables\nand nested values apply the same recursive credential redaction as `config dump`.\n\n`config set` supports its named scalar keys and the provider, route, and\nnotification commands. It refuses a key that nothing reads and suggests the\nnearest real key (`config set calm_mod on` names `calm_mode`). A settings.toml\nkey such as `calm_mode` or `tool_collapse` is validated and written to the\nuser-global settings.toml, not config.toml; `config get` reads it back from\nthere even when an old config.toml copy (which nothing reads) is still present,\nand names that copy so you can `config unset` it. Settings keys have no project\nscope, so `--project` refuses them. A config.toml key is checked against the\ntype its reader expects and stored as a TOML boolean or number where the\nreader needs one (`yolo = true`, `max_subagents = 4`); `reasoning_effort`\naccepts the same aliases as `/effort` (#6563). Other dotted writes fail before\nmodifying the file and name the TOML table to edit. For example, set a tools\ntimeout in the file as:\n\n```toml\n[tools]\nuser_input_timeout_seconds = 0\n```\n\n`codewhale config doctor` checks credential presence and endpoint shape, and\nwarns about each config.toml root key that nothing reads: a settings.toml key\nleft in config.toml is named as misplaced, and any other unread key gets a\ndid-you-mean. Keys read by any runtime reader are not reported. The warnings do\nnot fail the check, and a clean result does not validate every runtime setting's\nvalue (#6083, #6563).\n\n## Constitution, project instructions, and repo authority\n\nCodewhale has several instruction surfaces. They are deliberately separate so a\npersonal constitution, repo policy, project instructions, and runtime security\ncontrols do not blur together.\n\n- **Bundled global Constitution** — the compiled base law in the binary. It is\n the default floor for every session.\n- **User-global constitution** — the normal guided setup output. Manage it with\n `/constitution` or `/setup`; Codewhale stores structured data at\n `$CODEWHALE_HOME/constitution.json` (default `~/.codewhale/constitution.json`)\n and renders it into a separate `<codewhale_user_constitution>` prose block.\n This can express preferences and stop conditions, but it does not change\n runtime approval policy, sandbox, shell, network, trust, or MCP permissions.\n- **Repo-local constitution** — optional project policy in\n `.codewhale/constitution.json`, described below.\n- **`AGENTS.md`** — cross-agent **project instructions** (prose). This is the\n canonical file for \"how should an agent work in this repo.\" Run `/init` to\n scaffold one. Instruction files written for other tools (`CLAUDE.md`,\n `.claude/instructions.md`, `.cursorrules`, `.github/copilot-instructions.md`\n and similar) are read only when you opt in with the top-level\n `project_instruction_imports` key, for example `[\"claude\"]`, `[\"cursor\"]` or\n `[\"all\"]` (see `config.example.toml`); Codewhale warns when it finds one it\n did not load.\n\n Codewhale reads one instruction file per directory, from the repository\n root down to the workspace, and puts your global instruction file (such as\n `~/.codewhale/AGENTS.md`) in front of them. The nearest scope reads last\n and wins: the workspace file overrides its parents, and project files\n override the global file. Global and project instructions are budgeted\n separately in estimated tokens (see [Instruction budget](#instruction-budget-instruction_budget-6526)\n below): a scope over its budget drops whole files from the broadest end\n first, and a single file larger than the remaining budget keeps its head\n with a marker that names the bytes and tokens dropped. A file keeps at most\n its first 100 KiB, and a file larger than `file_max_bytesLine truncated
"sha256": "c36db36c85ad3be7d11a01dbdf0c74a5641844a62169431db5988a127a34cd69",
"content": "# Configuration\n\ncodewhale reads configuration from a TOML file plus environment variables.\nAt process startup it may also load literal built-in-provider credentials from\na workspace-local `.env` file. Use the tracked `.env.example` as the template;\ncopy it to `.env`, then add only credential values.\n\nA workspace is not configuration authority. Codewhale therefore ignores\nconfig/profile/home paths, provider/model/base-URL routing, MCP/plugin state,\napproval/sandbox/shell posture, executable paths, runtime settings, and every\nother non-credential `.env` entry. Variable expansion is rejected so a\nrepository cannot substitute an ambient secret into a credential value. Use\n`config.toml`, CLI flags, or values exported by the launching shell for those\nexplicit control-plane settings. `.env` is read from a stable regular-file\nhandle, is capped at 1 MiB, and symbolic links, reparse points, and multiply\nlinked files are rejected.\n\n## Reading and checking configuration from the CLI\n\n`codewhale config get <key>` reads scalar keys, whole tables such as `tools`,\nand nested paths such as `tools.user_input_timeout_seconds`. Displayed tables\nand nested values apply the same recursive credential redaction as `config dump`.\n\n`config set` supports its named scalar keys and the provider, route, and\nnotification commands. It refuses a key that nothing reads and suggests the\nnearest real key (`config set calm_mod on` names `calm_mode`). A settings.toml\nkey such as `calm_mode` or `tool_collapse` is validated and written to the\nuser-global settings.toml, not config.toml; `config get` reads it back from\nthere even when an old config.toml copy (which nothing reads) is still present,\nand names that copy so you can `config unset` it. Settings keys have no project\nscope, so `--project` refuses them. A config.toml key is checked against the\ntype its reader expects and stored as a TOML boolean or number where the\nreader needs one (`yolo = true`, `max_subagents = 4`); `reasoning_effort`\naccepts the same aliases as `/effort` (#6563). Other dotted writes fail before\nmodifying the file and name the TOML table to edit. For example, set a tools\ntimeout in the file as:\n\n```toml\n[tools]\nuser_input_timeout_seconds = 0\n```\n\n`codewhale config doctor` checks credential presence and endpoint shape, and\nwarns about each config.toml root key that nothing reads: a settings.toml key\nleft in config.toml is named as misplaced, and any other unread key gets a\ndid-you-mean. Keys read by any runtime reader are not reported. The warnings do\nnot fail the check, and a clean result does not validate every runtime setting's\nvalue (#6083, #6563).\n\n## Constitution, project instructions, and repo authority\n\nCodewhale has several instruction surfaces. They are deliberately separate so a\npersonal constitution, repo policy, project instructions, and runtime security\ncontrols do not blur together.\n\n- **Bundled global Constitution** — the compiled base law in the binary. It is\n the default floor for every session.\n- **User-global constitution** — the normal guided setup output. Manage it with\n `/constitution` or `/setup`; Codewhale stores structured data at\n `$CODEWHALE_HOME/constitution.json` (default `~/.codewhale/constitution.json`)\n and renders it into a separate `<codewhale_user_constitution>` prose block.\n This can express preferences and stop conditions, but it does not change\n runtime approval policy, sandbox, shell, network, trust, or MCP permissions.\n- **Repo-local constitution** — optional project policy in\n `.codewhale/constitution.json`, described below.\n- **`AGENTS.md`** — cross-agent **project instructions** (prose). This is the\n canonical file for \"how should an agent work in this repo.\" Run `/init` to\n scaffold one. Instruction files written for other tools (`CLAUDE.md`,\n `.claude/instructions.md`, `.cursorrules`, `.github/copilot-instructions.md`\n and similar) are read only when you opt in with the top-level\n `project_instruction_imports` key, for example `[\"claude\"]`, `[\"cursor\"]` or\n `[\"all\"]` (see `config.example.toml`); Codewhale warns when it finds one it\n did not load.\n\n Codewhale reads one instruction file per directory, from the repository\n root down to the workspace, and puts your global instruction file (such as\n `~/.codewhale/AGENTS.md`) in front of them. The nearest scope reads last\n and wins: the workspace file overrides its parents, and project files\n override the global file. Global and project instructions are budgeted\n separately in estimated tokens (see [Instruction budget](#instruction-budget-instruction_budget-6526)\n below): a scope over its budget drops whole files from the broadest end\n first, and a single file larger than the remaining budget keeps its head\n with a marker that names the bytes and tokens dropped. A file keeps at most\n its first 100 KiB, and a file larger than `file_max_bytesLine truncated
},
{
"path": "docs/LEGACY_PATHS.md",
"sha256": "89f7a556479d57911d9589af582c13a0c7494418d9d47f430d1bfa6c74ad3333",
"content": "# Legacy `.deepseek/` compatibility paths — audit & migration status (#3068)\n\n> 阅读简体中文版:[zh_hans/LEGACY_PATHS.md](zh_hans/LEGACY_PATHS.md)。\n\n## v0.9.10 cleanse ledger\n\nThis is the release-window decision record for residue that can otherwise look\ninterchangeable in a text search. A compatibility entry must name the contract\nthat still requires it; new production code must use a current path.\n\n| Class | Surface | Decision / contract |\n|---|---|---|\n| DELETE | Retired underwater build-version test override | Removed in `1cc4d1d1b`; the owner test was already deleted and the production-only build version remains authoritative. |\n| MIGRATE | Cross-task Agent Mail | `bd998495a` and managed-app `4ff7bc50d` replace guessed event aliases and same-session mailbox conflation with one durable typed runtime protocol. |\n| MIGRATE | TUI `status_message` writes | Active residue; touched flows must move to the typed toast/current-state owner. No new writes are allowed. |\n| COMPATIBILITY | `.deepseek/` state and stored provider aliases | Read fallback for upgraded installations and stored provider identity; writes remain Codewhale-only. Details are below. |\n| COMPATIBILITY | `agent_message` transcript/protocol decoding | Persisted transcripts and tool wire values require decoding; it is not the cross-task Agent Mail transport. |\n| COMPATIBILITY | Managed runtime envelope schema 1 | Previously persisted/enrolled runner replay; current local Codewhale Agent Mail producer is schema 2 and both normalize to the managed envelope. |\n| CURRENT | DeepSeek provider support | A real selectable provider, not product branding; provider-scoped names and configuration remain. |\n| CURRENT | Same-session subagent mailbox | Start/status/peek/message/followup/interrupt/wait/cancel stays scoped to a parent runtime session. |\n| CURRENT | Web locale dictionaries | The application and documentation routes have no page-local `isZh` branches; new copy continues through dictionaries. |\n| CURRENT | Safety, authorization, protocol, persistence, migration, and data-integrity tests | These protect external or durable contracts and are not copy/layout cleanup candidates. |\n\nSnapshot on 2026-08-19: `10,954` Rust `#[test]` attributes under\n`crates/tui`, `523` TUI `status_message` source lines, and zero page-local\n`isZh` files under `apps/web/app` in the paired managed-app candidate. Counts\nare inventory signals, not deletion targets.\n\nCodewhale was renamed from DeepSeek-TUI. To avoid breaking existing installs, the runtime reads\nstate from the new `~/.codewhale/` location but **falls back** to the legacy `~/.deepseek/` location,\nand always **writes** to `~/.codewhale/`. This doc audits each legacy reference and records a\nkeep / deprecate / remove decision so the migration is auditable.\n\n## The canonical resolver (use this for new code)\n\nState-dir resolution is consolidated in `crates/config/src/lib.rs`:\n\n| Symbol | Line | Purpose |\n|---|---|---|\n| `CODEWHALE_APP_DIR` / `LEGACY_APP_DIR` | 5369 | re-exported from `crates/paths` (defined at `crates/paths/src/lib.rs` 13 / 16) |\n| `codewhale_home()` | 5375 | `~/.codewhale` |\n| `legacy_deepseek_home()` | 5392 | `~/.deepseek` (legacy) |\n| `resolve_state_dir(subdir)` | 5434 | **read** path: `~/.codewhale/<subdir>`, falling back to `~/.deepseek/<subdir>` when only the legacy dir exists |\n| `ensure_state_dir(subdir)` | 5458 | **write** path: always creates under `~/.codewhale/<subdir>` |\n\nMigration contract: read-with-fallback, write-to-new. This preserves the v0.8.44 migration for\nusers who still have `~/.deepseek/` while steering all new writes to `~/.codewhale/`.\n\n## Per-path decisions\n\n**Decision for all legacy references below: keep-as-fallback.** Removing the `.deepseek` fallback\nwould strand users who upgraded in place and never re-ran onboarding. Revisit only after a release\nthat actively migrates `~/.deepseek/` → `~/.codewhale/` on first run and a deprecation window.\n\n| Reference | Routed through `resolve_state_dir`? | Decision |\n|---|---|---|\n| `config::resolve_state_dir` / `ensure_state_dir` | n/a (the resolver itself) | keep — canonical |\n| `crates/tui/src/skills/mod.rs` (`~/.deepseek/skills`) | no — hardcoded | keep-as-fallback; route through resolver in a follow-up refactor |\n| `crates/tui/src/prompts.rs` (`LEGACY_HANDOFF_RELATIVE_PATH = \".deepseek/handoff.md\"`) | no — explicit legacy const | keep — explicit legacy handoff fallback |\n| `crates/tui/src/workspace_trust.rs` | no — hardcoded | keep-as-fallback; follow-up |\n| `crates/tui/src/session_manager.rs` | no — hardcoded | keep-as-fallback; follow-up |\n| `crates/runtime/src/skill_state.rs` | no — hardcoded | keep-as-fallback; follow-up |\n| `crates/tui/src/tools/skill.rs` | no — hardcoded | keep-as-fallback; follow-up |\n| `crates/tui/src/snapshot/mod.rs` | no — hardcoded | keep-as-fallback; follow-up |\n| `crates/runtime/src/woLine truncated
"sha256": "614ee8108d5f9abeed798b9d8cf2acf23a38907a311218bd6f849f067e34be2c",
"content": "# Legacy `.deepseek/` compatibility paths — audit & migration status (#3068)\n\n> 阅读简体中文版:[zh_hans/LEGACY_PATHS.md](zh_hans/LEGACY_PATHS.md)。\n\n## v0.9.10 cleanse ledger\n\nThis is the release-window decision record for residue that can otherwise look\ninterchangeable in a text search. A compatibility entry must name the contract\nthat still requires it; new production code must use a current path.\n\n| Class | Surface | Decision / contract |\n|---|---|---|\n| DELETE | Retired underwater build-version test override | Removed in `1cc4d1d1b`; the owner test was already deleted and the production-only build version remains authoritative. |\n| MIGRATE | Cross-task Agent Mail | `bd998495a` and managed-app `4ff7bc50d` replace guessed event aliases and same-session mailbox conflation with one durable typed runtime protocol. |\n| MIGRATE | TUI `status_message` writes | Active residue; touched flows must move to the typed toast/current-state owner. No new writes are allowed. |\n| COMPATIBILITY | `.deepseek/` state and stored provider aliases | Read fallback for upgraded installations and stored provider identity; writes remain Codewhale-only. Details are below. |\n| COMPATIBILITY | `agent_message` transcript/protocol decoding | Persisted transcripts and tool wire values require decoding; it is not the cross-task Agent Mail transport. |\n| COMPATIBILITY | Managed runtime envelope schema 1 | Previously persisted/enrolled runner replay; current local Codewhale Agent Mail producer is schema 2 and both normalize to the managed envelope. |\n| CURRENT | DeepSeek provider support | A real selectable provider, not product branding; provider-scoped names and configuration remain. |\n| CURRENT | Same-session subagent mailbox | Start/status/peek/message/followup/interrupt/wait/cancel stays scoped to a parent runtime session. |\n| CURRENT | Web locale dictionaries | The application and documentation routes have no page-local `isZh` branches; new copy continues through dictionaries. |\n| CURRENT | Safety, authorization, protocol, persistence, migration, and data-integrity tests | These protect external or durable contracts and are not copy/layout cleanup candidates. |\n\nSnapshot on 2026-08-19: `10,954` Rust `#[test]` attributes under\n`crates/tui`, `523` TUI `status_message` source lines, and zero page-local\n`isZh` files under `apps/web/app` in the paired managed-app candidate. Counts\nare inventory signals, not deletion targets.\n\nCodewhale was renamed from DeepSeek-TUI. To avoid breaking existing installs, the runtime reads\nstate from the new `~/.codewhale/` location but **falls back** to the legacy `~/.deepseek/` location,\nand always **writes** to `~/.codewhale/`. This doc audits each legacy reference and records a\nkeep / deprecate / remove decision so the migration is auditable.\n\n## The canonical resolver (use this for new code)\n\nState-dir resolution is consolidated in `crates/config/src/lib.rs`:\n\n| Symbol | Line | Purpose |\n|---|---|---|\n| `CODEWHALE_APP_DIR` / `LEGACY_APP_DIR` | 5369 | re-exported from `crates/paths` (defined at `crates/paths/src/lib.rs` 13 / 16) |\n| `codewhale_home()` | 5375 | `~/.codewhale` |\n| `legacy_deepseek_home()` | 5392 | `~/.deepseek` (legacy) |\n| `resolve_state_dir(subdir)` | 5434 | **read** path: `~/.codewhale/<subdir>`, falling back to `~/.deepseek/<subdir>` when only the legacy dir exists |\n| `ensure_state_dir(subdir)` | 5458 | **write** path: always creates under `~/.codewhale/<subdir>` |\n\nMigration contract: read-with-fallback, write-to-new. This preserves the v0.8.44 migration for\nusers who still have `~/.deepseek/` while steering all new writes to `~/.codewhale/`.\n\n## Per-path decisions\n\n**Decision for all legacy references below: keep-as-fallback.** Removing the `.deepseek` fallback\nwould strand users who upgraded in place and never re-ran onboarding. Revisit only after a release\nthat actively migrates `~/.deepseek/` → `~/.codewhale/` on first run and a deprecation window.\n\n| Reference | Routed through `resolve_state_dir`? | Decision |\n|---|---|---|\n| `config::resolve_state_dir` / `ensure_state_dir` | n/a (the resolver itself) | keep — canonical |\n| `crates/tui/src/skills/mod.rs` (`~/.deepseek/skills`) | no — hardcoded | keep-as-fallback; route through resolver in a follow-up refactor |\n| `crates/tui/src/prompts.rs` (`LEGACY_HANDOFF_RELATIVE_PATH = \".deepseek/handoff.md\"`) | no — explicit legacy const | keep — explicit legacy handoff fallback |\n| `crates/tui/src/workspace_trust.rs` | no — hardcoded | keep-as-fallback; follow-up |\n| `crates/tui/src/session_manager.rs` | no — hardcoded | keep-as-fallback; follow-up |\n| `crates/runtime/src/skill_state.rs` | no — hardcoded | keep-as-fallback; follow-up |\n| `crates/tui/src/tools/skill.rs` | no — hardcoded | keep-as-fallback; follow-up |\n| `crates/snapshot/src/lib.rs` | no — hardcoded | keep-as-fallback; follow-up |\n| `crates/runtime/src/workspLine truncated
}
]
},
@@ -80,8 +80,8 @@
"sources": [
{
"path": "docs/MODES.md",
"sha256": "06bc6d00b279d7a3d0479b370893a900139e593a6a59db6b0d574e621f86363e",
"content": "# Modes and Permission Postures\n\n> 阅读简体中文版:[zh_hans/MODES.md](zh_hans/MODES.md)\n\nCodewhale has three related concepts:\n\n- **TUI mode**: what kind of visible interaction you're in (Plan/Work/Operate).\n- **Permission posture**: how aggressively the UI asks before executing tools.\n- **Workflow**: named steps that coordinate sub-agents, with dependencies and\n results, available in any TUI mode.\n\nModel selection is separate. `--model auto` and `/model auto` route each turn to\na concrete model and thinking level; they are not TUI modes and are not part of\nthe `Tab` cycle.\n\nWorkflow runs named steps through the same sub-agent runtime; Fleet manages\ntheir saved roles and model assignments. A workflow adds ordering, result\nhandoffs, gates and a progress view. The active mode and permission posture\nstill control what each step may execute.\n\nBefore assigning steps, query `agent(action=\"roster\")` for the saved Fleet\nmodels and roles. A plan child can select a listed model with `model`, or use\nthe saved `role`/`profile` assignment. An Exact Fleet fixes each member's route.\n\n## TUI Modes\n\nPress `Tab` to complete composer menus or cycle through the visible modes\nwhen the composer is empty: **Plan → Work → Operate → Plan**. `Tab` never sends\nor queues composer text; use `Enter` to send or queue it.\nPress `Shift+Tab` to cycle permission posture (Ask → Auto-Review → Full Access).\nEvery change of effective permission writes a `Permissions changed to …` line\nin the transcript.\nPress `Ctrl+T` to cycle reasoning effort.\nRun `/mode` to open the mode picker, or switch directly with `/mode work`,\n`/mode plan`, or `/mode operate`.\n\n- **Plan**: design-first prompting. The stable primitive names remain familiar, but the runtime centrally refuses file mutation and shell execution. Read-only inspection and policy-allowed research, including deferred Web search/fetch, remain available. When a Plan turn successfully finishes a nonempty response, including a prose-only plan, the TUI asks how to continue: Work (Ask), Work (Auto-Review), or keep planning. Work sends the exact approved response as a normal follow-up and preserves that turn's existing To-dos, or adds one pending plan item without replacing unrelated work.\n- **Work** (internally `agent`): ordinary multi-step execution. The first-turn toolbox includes `read`, `write`, `edit`, `file_search`, `bash`, `agent`, `workflow`, `todo_write`, and `load_skill`, plus `create_goal`, `get_goal`, `update_goal`, and `task_shell_wait` so goal and background-shell controls are available without discovery. The agent calls `create_goal` when a request is a durable objective; the host never infers a goal from wording, and the contract must not tell the model otherwise. Approval, sandbox, repository law, and managed policy decide what may execute.\n- **Operate**: Work at full strength, the \"ultra\" mode. Same tools and authority as Work; the difference is how they are used. Each substantive request becomes a goal (`create_goal`), the plan stays visible (`todo_write`), multi-part work runs through `workflow` and parallel `agent` workers, non-trivial changes get an independent reviewer before they are called done, long commands keep running in the background, and recurring or watch-type work is offered as an `automation` (created after approval). It keeps going until the goal is verified, blocked on you, or paused.\n\n`Act` and `/mode act` remain compatibility aliases for Work. Saved settings\nstill normalize to the internal value `agent`.\n\n### Tool availability by mode\n\n| Tool family | Plan | Work | Operate |\n|:---|:---:|:---:|:---:|\n| `read` and policy-allowed deferred research tools | yes | yes | yes |\n| `write` and `edit` | visible names; execution denied | approval- and policy-gated | same as Work |\n| `bash` | visible name; execution denied | approval- and policy-gated | same as Work; delegation is preferred when parallelism or isolation helps |\n| `agent` | yes, subject to child-depth authority | yes, subject to child-depth authority | yes, subject to child-depth authority |\n| Deferred native, MCP, and plugin tools | discoverable through `tool_search` when policy permits | same | same |\n| Paid or external-service tools | follows permission posture | follows permission posture | follows permission posture |\n| Access outside the workspace root | explicit trusted paths only | only through trusted paths or trust mode | same trusted-path/trust policy as Work; fleet profiles never widen it |\n\nOperate changes scheduling emphasis, not authority. It neither adds a\nmode-specific tool denial nor bypasses the active approval, sandbox, shell,\nask-rule, repository-law, or managed-policy boundary. Plan remains the\nmode-specific execution boundary for shell and write-capable tools; that\nauthority difference does not require a different primitive vocabulary.\n\n### Operate loop (one screen)\n\n```text\nUser Line truncated
"sha256": "e327b2042c9b782660cb0d6de9f4208364537ef1052a456a45332a61b1ee2468",
"content": "# Modes and Permission Postures\n\n> 阅读简体中文版:[zh_hans/MODES.md](zh_hans/MODES.md)\n\nCodewhale has three related concepts:\n\n- **TUI mode**: what kind of visible interaction you're in (Plan/Work/Operate).\n- **Permission posture**: how aggressively the UI asks before executing tools.\n- **Workflow**: named steps that coordinate sub-agents, with dependencies and\n results, available in any TUI mode.\n\nModel selection is separate. `--model auto` and `/model auto` route each turn to\na concrete model and thinking level; they are not TUI modes and are not part of\nthe `Tab` cycle.\n\nWorkflow runs named steps through the same sub-agent runtime; Fleet manages\ntheir saved roles and model assignments. A workflow adds ordering, result\nhandoffs, gates and a progress view. The active mode and permission posture\nstill control what each step may execute.\n\nBefore assigning steps, query `agent(action=\"roster\")` for the saved Fleet\nmodels and roles. A plan child can select a listed model with `model`, or use\nthe saved `role`/`profile` assignment. An Exact Fleet fixes each member's route.\n\n## TUI Modes\n\nPress `Tab` to complete composer menus or cycle through the visible modes\nwhen the composer is empty: **Plan → Work → Operate → Plan**. `Tab` never sends\nor queues composer text; use `Enter` to send or queue it.\nPress `Shift+Tab` to cycle permission posture (Ask → Auto-Review → Full Access).\nEvery change of effective permission writes a `Permissions changed to …` line\nin the transcript.\nPress `Ctrl+T` to cycle reasoning effort.\nRun `/mode` to open the mode picker, or switch directly with `/mode work`,\n`/mode plan`, or `/mode operate`.\n\n- **Plan**: design-first prompting. The stable primitive names remain familiar, but the runtime centrally refuses file mutation and shell execution. Read-only inspection and policy-allowed research, including deferred Web search/fetch, remain available. When a Plan turn successfully finishes a nonempty response, including a prose-only plan, the TUI asks how to continue: Work (Ask), Work (Auto-Review), or keep planning. Work sends the exact approved response as a normal follow-up and preserves that turn's existing To-dos, or adds one pending plan item without replacing unrelated work.\n- **Work** (internally `agent`): ordinary multi-step execution. The first-turn toolbox includes `read`, `write`, `edit`, `file_search`, `bash`, `agent`, `workflow`, `todo_write`, and `load_skill`, plus `create_goal`, `get_goal`, `update_goal`, and `task_shell_wait` so goal and background-shell controls are available without discovery. The agent calls `create_goal` when a request is a durable objective; the host never infers a goal from wording, and the contract must not tell the model otherwise. Approval, sandbox, repository law, and managed policy decide what may execute.\n- **Operate**: Work at full strength, the \"ultra\" mode. Same tools and authority as Work; the difference is how they are used. Each substantive request becomes a goal (`create_goal`), the plan stays visible (`todo_write`), multi-part work runs through `workflow` and parallel `agent` workers, non-trivial changes get an independent reviewer before they are called done, long commands keep running in the background, and recurring or watch-type work is offered as an `automation` (created after approval). It keeps going until the goal is verified, blocked on you, or paused.\n\n`Act` and `/mode act` remain compatibility aliases for Work. Saved settings\nstill normalize to the internal value `agent`.\n\n### Tool availability by mode\n\n| Tool family | Plan | Work | Operate |\n|:---|:---:|:---:|:---:|\n| `read` and policy-allowed deferred research tools | yes | yes | yes |\n| `write` and `edit` | visible names; execution denied | approval- and policy-gated | same as Work |\n| `bash` | visible name; execution denied | approval- and policy-gated | same as Work; delegation is preferred when parallelism or isolation helps |\n| `agent` | yes, subject to child-depth authority | yes, subject to child-depth authority | yes, subject to child-depth authority |\n| Deferred native, MCP, and plugin tools | discoverable through `tool_search` when policy permits | same | same |\n| Paid or external-service tools | follows permission posture | follows permission posture | follows permission posture |\n| Access outside the workspace root | explicit trusted paths only | only through trusted paths or trust mode | same trusted-path/trust policy as Work; fleet profiles never widen it |\n\nOperate changes scheduling emphasis, not authority. It neither adds a\nmode-specific tool denial nor bypasses the active approval, sandbox, shell,\nask-rule, repository-law, or managed-policy boundary. Plan remains the\nmode-specific execution boundary for shell and write-capable tools; that\nauthority difference does not require a different primitive vocabulary.\n\n### Operate loop (one screen)\n\n```text\nUser Line truncated
}
]
},
@@ -96,8 +96,8 @@
},
{
"path": "docs/CONFIGURATION.md",
"sha256": "3e076746af15d2c2f25bd60d86648c95ca17fc98ec0d9b1bd4d3a025958dd5f3",
"content": "# Configuration\n\ncodewhale reads configuration from a TOML file plus environment variables.\nAt process startup it may also load literal built-in-provider credentials from\na workspace-local `.env` file. Use the tracked `.env.example` as the template;\ncopy it to `.env`, then add only credential values.\n\nA workspace is not configuration authority. Codewhale therefore ignores\nconfig/profile/home paths, provider/model/base-URL routing, MCP/plugin state,\napproval/sandbox/shell posture, executable paths, runtime settings, and every\nother non-credential `.env` entry. Variable expansion is rejected so a\nrepository cannot substitute an ambient secret into a credential value. Use\n`config.toml`, CLI flags, or values exported by the launching shell for those\nexplicit control-plane settings. `.env` is read from a stable regular-file\nhandle, is capped at 1 MiB, and symbolic links, reparse points, and multiply\nlinked files are rejected.\n\n## Reading and checking configuration from the CLI\n\n`codewhale config get <key>` reads scalar keys, whole tables such as `tools`,\nand nested paths such as `tools.user_input_timeout_seconds`. Displayed tables\nand nested values apply the same recursive credential redaction as `config dump`.\n\n`config set` supports its named scalar keys and the provider, route, and\nnotification commands. It refuses a key that nothing reads and suggests the\nnearest real key (`config set calm_mod on` names `calm_mode`). A settings.toml\nkey such as `calm_mode` or `tool_collapse` is validated and written to the\nuser-global settings.toml, not config.toml; `config get` reads it back from\nthere even when an old config.toml copy (which nothing reads) is still present,\nand names that copy so you can `config unset` it. Settings keys have no project\nscope, so `--project` refuses them. A config.toml key is checked against the\ntype its reader expects and stored as a TOML boolean or number where the\nreader needs one (`yolo = true`, `max_subagents = 4`); `reasoning_effort`\naccepts the same aliases as `/effort` (#6563). Other dotted writes fail before\nmodifying the file and name the TOML table to edit. For example, set a tools\ntimeout in the file as:\n\n```toml\n[tools]\nuser_input_timeout_seconds = 0\n```\n\n`codewhale config doctor` checks credential presence and endpoint shape, and\nwarns about each config.toml root key that nothing reads: a settings.toml key\nleft in config.toml is named as misplaced, and any other unread key gets a\ndid-you-mean. Keys read by any runtime reader are not reported. The warnings do\nnot fail the check, and a clean result does not validate every runtime setting's\nvalue (#6083, #6563).\n\n## Constitution, project instructions, and repo authority\n\nCodewhale has several instruction surfaces. They are deliberately separate so a\npersonal constitution, repo policy, project instructions, and runtime security\ncontrols do not blur together.\n\n- **Bundled global Constitution** — the compiled base law in the binary. It is\n the default floor for every session.\n- **User-global constitution** — the normal guided setup output. Manage it with\n `/constitution` or `/setup`; Codewhale stores structured data at\n `$CODEWHALE_HOME/constitution.json` (default `~/.codewhale/constitution.json`)\n and renders it into a separate `<codewhale_user_constitution>` prose block.\n This can express preferences and stop conditions, but it does not change\n runtime approval policy, sandbox, shell, network, trust, or MCP permissions.\n- **Repo-local constitution** — optional project policy in\n `.codewhale/constitution.json`, described below.\n- **`AGENTS.md`** — cross-agent **project instructions** (prose). This is the\n canonical file for \"how should an agent work in this repo.\" Run `/init` to\n scaffold one. Instruction files written for other tools (`CLAUDE.md`,\n `.claude/instructions.md`, `.cursorrules`, `.github/copilot-instructions.md`\n and similar) are read only when you opt in with the top-level\n `project_instruction_imports` key, for example `[\"claude\"]`, `[\"cursor\"]` or\n `[\"all\"]` (see `config.example.toml`); Codewhale warns when it finds one it\n did not load.\n\n Codewhale reads one instruction file per directory, from the repository\n root down to the workspace, and puts your global instruction file (such as\n `~/.codewhale/AGENTS.md`) in front of them. The nearest scope reads last\n and wins: the workspace file overrides its parents, and project files\n override the global file. Global and project instructions are budgeted\n separately in estimated tokens (see [Instruction budget](#instruction-budget-instruction_budget-6526)\n below): a scope over its budget drops whole files from the broadest end\n first, and a single file larger than the remaining budget keeps its head\n with a marker that names the bytes and tokens dropped. A file keeps at most\n its first 100 KiB, and a file larger than `file_max_bytesLine truncated
"sha256": "c36db36c85ad3be7d11a01dbdf0c74a5641844a62169431db5988a127a34cd69",
"content": "# Configuration\n\ncodewhale reads configuration from a TOML file plus environment variables.\nAt process startup it may also load literal built-in-provider credentials from\na workspace-local `.env` file. Use the tracked `.env.example` as the template;\ncopy it to `.env`, then add only credential values.\n\nA workspace is not configuration authority. Codewhale therefore ignores\nconfig/profile/home paths, provider/model/base-URL routing, MCP/plugin state,\napproval/sandbox/shell posture, executable paths, runtime settings, and every\nother non-credential `.env` entry. Variable expansion is rejected so a\nrepository cannot substitute an ambient secret into a credential value. Use\n`config.toml`, CLI flags, or values exported by the launching shell for those\nexplicit control-plane settings. `.env` is read from a stable regular-file\nhandle, is capped at 1 MiB, and symbolic links, reparse points, and multiply\nlinked files are rejected.\n\n## Reading and checking configuration from the CLI\n\n`codewhale config get <key>` reads scalar keys, whole tables such as `tools`,\nand nested paths such as `tools.user_input_timeout_seconds`. Displayed tables\nand nested values apply the same recursive credential redaction as `config dump`.\n\n`config set` supports its named scalar keys and the provider, route, and\nnotification commands. It refuses a key that nothing reads and suggests the\nnearest real key (`config set calm_mod on` names `calm_mode`). A settings.toml\nkey such as `calm_mode` or `tool_collapse` is validated and written to the\nuser-global settings.toml, not config.toml; `config get` reads it back from\nthere even when an old config.toml copy (which nothing reads) is still present,\nand names that copy so you can `config unset` it. Settings keys have no project\nscope, so `--project` refuses them. A config.toml key is checked against the\ntype its reader expects and stored as a TOML boolean or number where the\nreader needs one (`yolo = true`, `max_subagents = 4`); `reasoning_effort`\naccepts the same aliases as `/effort` (#6563). Other dotted writes fail before\nmodifying the file and name the TOML table to edit. For example, set a tools\ntimeout in the file as:\n\n```toml\n[tools]\nuser_input_timeout_seconds = 0\n```\n\n`codewhale config doctor` checks credential presence and endpoint shape, and\nwarns about each config.toml root key that nothing reads: a settings.toml key\nleft in config.toml is named as misplaced, and any other unread key gets a\ndid-you-mean. Keys read by any runtime reader are not reported. The warnings do\nnot fail the check, and a clean result does not validate every runtime setting's\nvalue (#6083, #6563).\n\n## Constitution, project instructions, and repo authority\n\nCodewhale has several instruction surfaces. They are deliberately separate so a\npersonal constitution, repo policy, project instructions, and runtime security\ncontrols do not blur together.\n\n- **Bundled global Constitution** — the compiled base law in the binary. It is\n the default floor for every session.\n- **User-global constitution** — the normal guided setup output. Manage it with\n `/constitution` or `/setup`; Codewhale stores structured data at\n `$CODEWHALE_HOME/constitution.json` (default `~/.codewhale/constitution.json`)\n and renders it into a separate `<codewhale_user_constitution>` prose block.\n This can express preferences and stop conditions, but it does not change\n runtime approval policy, sandbox, shell, network, trust, or MCP permissions.\n- **Repo-local constitution** — optional project policy in\n `.codewhale/constitution.json`, described below.\n- **`AGENTS.md`** — cross-agent **project instructions** (prose). This is the\n canonical file for \"how should an agent work in this repo.\" Run `/init` to\n scaffold one. Instruction files written for other tools (`CLAUDE.md`,\n `.claude/instructions.md`, `.cursorrules`, `.github/copilot-instructions.md`\n and similar) are read only when you opt in with the top-level\n `project_instruction_imports` key, for example `[\"claude\"]`, `[\"cursor\"]` or\n `[\"all\"]` (see `config.example.toml`); Codewhale warns when it finds one it\n did not load.\n\n Codewhale reads one instruction file per directory, from the repository\n root down to the workspace, and puts your global instruction file (such as\n `~/.codewhale/AGENTS.md`) in front of them. The nearest scope reads last\n and wins: the workspace file overrides its parents, and project files\n override the global file. Global and project instructions are budgeted\n separately in estimated tokens (see [Instruction budget](#instruction-budget-instruction_budget-6526)\n below): a scope over its budget drops whole files from the broadest end\n first, and a single file larger than the remaining budget keeps its head\n with a marker that names the bytes and tokens dropped. A file keeps at most\n its first 100 KiB, and a file larger than `file_max_bytesLine truncated
}
]
},
@@ -187,8 +187,8 @@
},
{
"path": "docs/TELEMETRY.md",
"sha256": "6e69aec8fe595b6d40bd63c71cfaea6cc980ca7b1adaa09b9e4920c50bfe3d53",
"content": "# Codewhale product telemetry\n\n> 阅读简体中文版:[zh_hans/TELEMETRY.md](zh_hans/TELEMETRY.md)\n\n**Usage analytics are on by default in the current source.** The first\ninteractive launch gives a localized, nonblocking disclosure naming **Codewhale\nand PostHog**, with a direct route to turn counting off. Notice version `5`\ndescribes this policy; displaying it is not recorded as human acceptance.\nMissing an older acceptance record does not turn a fresh installation off.\nExisting explicit opt-outs remain off across upgrades, and unreadable privacy\nstate fails closed. Environment and CLI kill switches still take precedence.\n\nRead the disclosure with `codewhale config telemetry`. Turn usage off in\n`/settings` or run `codewhale config set telemetry false`. Turning it back on\nin Settings or with `codewhale config set telemetry true` updates the existing\ndurable preference and privacy records for new sessions. The compatibility\ncommand `codewhale config telemetry --accept-notice 5` remains available, but\nis not required for a fresh installation. Collection never includes work content or credentials.\n\nThe PostHog sink is optional and inert until the ingest operator explicitly\nconfigures a project token and approved regional host, and records staging\nproof that the actual egress path does not forward the original client IP.\nSource readiness does not establish a deployment, an activated processor,\nan egress receipt, or a retention setting.\n\nCodewhale does not collect conversations, code, prompts, files, file/repo/branch\nnames, model content, or credentials. It sends no per-turn or per-tool timeline.\nIt does send the closed, aggregate schema below: version and platform classes,\nsession duration/outcome, feature/error counters, and a random install id that\nrotates every 90 days.\n\n**There is now a real endpoint.** An enabled session sends its batches to the\nfirst-party ingest service at `https://telemetry.codewhale.net/v1/telemetry`,\nwhich is the shipped default for `telemetry_endpoint`. What that service is,\nwhat it stores, and what it structurally cannot store is spelled out in \"What\nthe endpoint does\" below.\n\n**To send nothing anywhere, turn telemetry off** (see \"Turning it off\"). To stay\nenabled but contact nobody, set `telemetry_endpoint = \"\"`: batches are then\nappended to `$CODEWHALE_HOME/telemetry/dryrun.jsonl` on your own machine, byte\nfor byte what the server would have received, and no HTTP client is ever\nconstructed. That file is how you audit this document against reality.\n\nThis document is the schema. It is not a summary of the schema: a test in\n`crates/telemetry` parses the field names out of this file and asserts set\nequality against the structs the serializer actually uses, so a field that is\nhere and not in the code — or in the code and not here — fails the build.\n\nLocal saved sessions and `codewhale metrics` remain available with telemetry\ndisabled and require no account or hosted reporting service. Disabling telemetry\ndoes not disable local usage receipts.\n\n## Turning it off\n\nThere are two off switches and they do different things. Both stop collection\ncompletely; only one of them erases anything.\n\n```sh\ncodewhale config set telemetry false # opt out: stops collection and erases state\nCODEWHALE_TELEMETRY=0 codewhale # kill switch: stops collection, erases nothing\ncodewhale --telemetry false # the same kill switch, for one command\n```\n\n**`telemetry = false` in the config file is the opt-out.** It is a floor:\n`--telemetry true` and `CODEWHALE_TELEMETRY=1` both lose to it, because a\nsetting you can undo by accident from a wrapper script is not a setting. It\ndeletes the random install id, truncates every buffered event and every dry-run\nrecord, and writes a tombstone. Appends, identity/state writes, and delivery all\nshare the wipe's ordering lock, so once opt-out returns no pre-opt-out write or\nPOST remains in flight. If any part of that wipe fails, the tombstone is still\nthere and the buffer is undrainable — a failed wipe fails closed. Every later\nrun re-asserts the same tombstone for as long as the setting stands, so it\nsurvives. Turning telemetry back on in `/settings` records that explicit preference\nin both existing privacy registers; a fresh launch can then clear the tombstone. Nothing buffered before that point is\never sent.\n\n**The environment variable and the flag are kill switches, not opt-outs.**\nTelemetry is off for the run, nothing is written, nothing is sent — and\nnothing on disk is touched or deleted. That is deliberate: a harness or agent\nthat sets `CODEWHALE_TELEMETRY=0` for one command must not silently discard the\ninstall id and the dry-run records of the person who owns the machine. If you\nwant the erasing kind, use the config file.\n\n`CODEWHALE_TELEMETRY` (and its `DEEPSEEK_TELEMETRY` alias) accepts\n`0`, `1`, `true`, `false`, `yes`, `no`, Line truncated
"sha256": "ead511b17a4986600cbe2effc9d4d91b4743958e9ef0cf0fe24ae873a58173a2",
"content": "# Codewhale product telemetry\n\n> 阅读简体中文版:[zh_hans/TELEMETRY.md](zh_hans/TELEMETRY.md)\n\n**Usage analytics are on by default in the current source.** The first\ninteractive launch gives a localized, nonblocking disclosure naming **Codewhale\nand PostHog**, with a direct route to turn counting off. Notice version `5`\ndescribes this policy; displaying it is not recorded as human acceptance.\nMissing an older acceptance record does not turn a fresh installation off.\nExisting explicit opt-outs remain off across upgrades, and unreadable privacy\nstate fails closed. Environment and CLI kill switches still take precedence.\n\nRead the disclosure with `codewhale config telemetry`. Turn usage off in\n`/settings` or run `codewhale config set telemetry false`. Turning it back on\nin Settings or with `codewhale config set telemetry true` updates the existing\ndurable preference and privacy records for new sessions. The compatibility\ncommand `codewhale config telemetry --accept-notice 5` remains available, but\nis not required for a fresh installation. Collection never includes work content or credentials.\n\nThe PostHog sink is optional and inert until the ingest operator explicitly\nconfigures a project token and approved regional host, and records staging\nproof that the actual egress path does not forward the original client IP.\nSource readiness does not establish a deployment, an activated processor,\nan egress receipt, or a retention setting.\n\nCodewhale does not collect conversations, code, prompts, files, file/repo/branch\nnames, model content, or credentials. It sends no per-turn or per-tool timeline.\nIt does send the closed, aggregate schema below: version and platform classes,\nsession duration/outcome, feature/error counters, and a random install id that\nrotates every 90 days.\n\n**There is now a real endpoint.** An enabled session sends its batches to the\nfirst-party ingest service at `https://telemetry.codewhale.net/v1/telemetry`,\nwhich is the shipped default for `telemetry_endpoint`. What that service is,\nwhat it stores, and what it structurally cannot store is spelled out in \"What\nthe endpoint does\" below.\n\n**To send nothing anywhere, turn telemetry off** (see \"Turning it off\"). To stay\nenabled but contact nobody, set `telemetry_endpoint = \"\"`: batches are then\nappended to `$CODEWHALE_HOME/telemetry/dryrun.jsonl` on your own machine, byte\nfor byte what the server would have received, and no HTTP client is ever\nconstructed. That file is how you audit this document against reality.\n\nThis document is the schema. It is not a summary of the schema: a test in\n`crates/telemetry` parses the field names out of this file and asserts set\nequality against the structs the serializer actually uses, so a field that is\nhere and not in the code — or in the code and not here — fails the build.\n\nLocal saved sessions and `codewhale metrics` remain available with telemetry\ndisabled and require no account or hosted reporting service. Disabling telemetry\ndoes not disable local usage receipts.\n\n## Turning it off\n\nThere are two off switches and they do different things. Both stop collection\ncompletely; only one of them erases anything.\n\n```sh\ncodewhale config set telemetry false # opt out: stops collection and erases state\nCODEWHALE_TELEMETRY=0 codewhale # kill switch: stops collection, erases nothing\ncodewhale --telemetry false # the same kill switch, for one command\n```\n\n**`telemetry = false` in the config file is the opt-out.** It is a floor:\n`--telemetry true` and `CODEWHALE_TELEMETRY=1` both lose to it, because a\nsetting you can undo by accident from a wrapper script is not a setting. It\ndeletes the random install id, truncates every buffered event and every dry-run\nrecord, and writes a tombstone. Appends, identity/state writes, and delivery all\nshare the wipe's ordering lock, so once opt-out returns no pre-opt-out write or\nPOST remains in flight. If any part of that wipe fails, the tombstone is still\nthere and the buffer is undrainable — a failed wipe fails closed. Every later\nrun re-asserts the same tombstone for as long as the setting stands, so it\nsurvives. Turning telemetry back on in `/settings` records that explicit preference\nin both existing privacy registers; a fresh launch can then clear the tombstone. Nothing buffered before that point is\never sent.\n\n**The environment variable and the flag are kill switches, not opt-outs.**\nTelemetry is off for the run, nothing is written, nothing is sent — and\nnothing on disk is touched or deleted. That is deliberate: a harness or agent\nthat sets `CODEWHALE_TELEMETRY=0` for one command must not silently discard the\ninstall id and the dry-run records of the person who owns the machine. If you\nwant the erasing kind, use the config file.\n\n`CODEWHALE_TELEMETRY` (and its `DEEPSEEK_TELEMETRY` alias) accepts\n`0`, `1`, `true`, `false`, `yes`, `no`, Line truncated
},
{
"path": "docs/public-surface-facts.json",
@@ -208,8 +208,8 @@
"sources": [
{
"path": "docs/MCP.md",
"sha256": "f9516ede4e2b90239069600c78a36070f89410cb5bd40b8d9b7d1926303de0a5",
"content": "# MCP (External Tool Servers)\n\nIn the terminal, `/mcp` (also `/mcps`) opens **Extensions → MCP**. Enter opens the selected server’s recovery action or read-only details. An empty inventory offers server suggestions; browsing them installs nothing. Explicit subcommands such as `/mcp status`, `/mcp doctor`, `/mcp login`, and `/mcp add` retain their existing behavior.\n\n\n> 阅读简体中文版:[zh_hans/MCP.md](zh_hans/MCP.md)\n\ncodewhale can load additional tools via MCP (Model Context Protocol). MCP servers can be local stdio processes that the TUI starts, or remote URL-based servers that speak Streamable HTTP with legacy SSE fallback.\n\nBrowsing note:\n- `Web` is the canonical, deferred built-in browsing tool; it provides\n `search`, `fetch`, and `wait` actions when network policy permits.\n- `web_search`, `fetch_url`, and `wait_for_dev_server` are hidden replay-only\n aliases. New prompts and integrations should use `Web`.\n\nServer mode note:\n- `codewhale serve --mcp` runs the MCP stdio server.\n- `codewhale serve --http` runs the runtime HTTP/SSE API (separate mode).\n- `codewhale mcp-server` is an equivalent stdio entrypoint on the same\n consolidated runtime.\n\nIn 0.10.1, the old child-server aggregation proxy is removed. `mcp-server`\nnow uses the same native tool server as `serve --mcp`; it does not launch the\nlegacy `mcp.server_definitions` list. Saved definitions are left intact. Configure\nexternal servers in `mcp.json` for the existing MCP client, or connect to those\nservers directly from your external client. Native server write tools remain\nwithheld unless the operator explicitly permits them in its server configuration.\n\nThe native stdio server supports MCP **2026-07-28** with required per-request\nmetadata and `server/discover`; no initialization handshake is needed for that\nrevision. It also supports the explicit legacy revisions **2024-11-05**,\n**2025-03-26**, **2025-06-18** and **2025-11-25** through `initialize`.\nInitialization never selects a modern revision; unsupported initialization\nversions fall back to 2025-11-25. Legacy callers should send\n`notifications/initialized`, but calls may proceed after a valid initialize\nresponse. This describes Codewhale as a server; outbound MCP client version\nsupport is configured separately.\n\n`resources/list` pages through the effective native skill instruction catalog,\nand `resources/read` returns the accepted instruction body for an exact listed\n`codewhale://skills/...` URI. Discovery follows the configured skill roots,\nprecedence and workspace trust; disabled skills are omitted. Reads supply inert\nMarkdown and never activate a skill. These are instruction-body resources, not\noriginal SKILL.md files or complete skill packages. Plugin skills, arbitrary\nfilesystem paths and session transcripts are not exposed by this catalog.\nThe old workspace/session entries, which had no resource reader, are removed.\nResource templates are empty. Lists and reads use private caching with a zero\nTTL; restart pagination if a changed catalog invalidates a cursor.\n\nOnly tools and resources are advertised. Subscriptions, list-change\nnotifications, the Skills extension, Apps, tasks and interactive approval are\nnot implemented by this server. Tool execution remains serialized through the\nRust registry, and request cancellation reaches the existing tool context.\nKeep stdin open until responses arrive; closing it cancels outstanding work.\nMalformed JSON and oversized input frames are discarded by the bounded SDK\ntransport, while well-formed invalid JSON-RPC receives an error.\n\n## Setup wizard vs manual MCP setup (#3407)\n\nThe `/setup` hub includes an optional **Tools and MCP**\nstep. That step is discovery/readiness only:\n\n| Wizard can do | Still requires manual / explicit action |\n| --- | --- |\n| Show configured servers as `healthy` / `needs_config` / `off` | Start or connect MCP servers |\n| Report config path presence (global + project) | Write or edit `mcp.json` contents |\n| Safe static health probe (missing command/url, broken absolute path, missing bearer env) | `codewhale mcp validate`, live connect, OAuth login |\n| Point at safe on-ramps (`/mcp`, `codewhale mcp init`, `codewhale doctor`) | Install community skills, trust skills, enable plugins |\n| Share Hotbar source counts from the same skill/MCP adapters (#3399) | Bind Hotbar slots (Hotbar step / `H`) |\n| Record optional/`needs_action` setup_state without blocking first-run | Anything that spawns processes or installs packages |\n\nEmpty inventory is **not** an error: first-run users see “nothing configured\nyet, that’s fine.” Failing or incomplete configured servers surface as\n`needs_config` with an actionable hint and never block setup completion.\nEnumeration never executes MCP/plugin commands beyond the static probe.\nSummaries redact commands, args, env, headers, and tokens.\n\n`codewhale doctor` reports MCP/skills/tools/plugLine truncated
"sha256": "ee78f41afbe898dc7fd8b782a9055ecee4b90cb6c66a841a3a35d839a5474465",
"content": "# MCP (External Tool Servers)\n\nIn the terminal, `/mcp` (also `/mcps`) opens **Extensions → MCP**. Enter opens the selected server’s recovery action or read-only details. An empty inventory offers server suggestions; browsing them installs nothing. Explicit subcommands such as `/mcp status`, `/mcp doctor`, `/mcp login`, and `/mcp add` retain their existing behavior.\n\n\n> 阅读简体中文版:[zh_hans/MCP.md](zh_hans/MCP.md)\n\ncodewhale can load additional tools via MCP (Model Context Protocol). MCP servers can be local stdio processes that the TUI starts, or remote URL-based servers that speak Streamable HTTP with legacy SSE fallback.\n\nBrowsing note:\n- `Web` is the canonical, deferred built-in browsing tool; it provides\n `search`, `fetch`, and `wait` actions when network policy permits.\n- `web_search`, `fetch_url`, and `wait_for_dev_server` are hidden replay-only\n aliases. New prompts and integrations should use `Web`.\n\nServer mode note:\n- `codewhale serve --mcp` runs the MCP stdio server.\n- `codewhale serve --http` runs the runtime HTTP/SSE API (separate mode).\n- `codewhale mcp-server` is an equivalent stdio entrypoint on the same\n consolidated runtime.\n\nIn 0.10.1, the old child-server aggregation proxy is removed. `mcp-server`\nnow uses the same native tool server as `serve --mcp`; it does not launch the\nlegacy `mcp.server_definitions` list. Saved definitions are left intact. Configure\nexternal servers in `mcp.json` for the existing MCP client, or connect to those\nservers directly from your external client. Native server write tools remain\nwithheld unless the operator explicitly permits them in its server configuration.\n\nThe native stdio server supports MCP **2026-07-28** with required per-request\nmetadata and `server/discover`; no initialization handshake is needed for that\nrevision. It also supports the explicit legacy revisions **2024-11-05**,\n**2025-03-26**, **2025-06-18** and **2025-11-25** through `initialize`.\nInitialization never selects a modern revision; unsupported initialization\nversions fall back to 2025-11-25. Legacy callers should send\n`notifications/initialized`, but calls may proceed after a valid initialize\nresponse. This describes Codewhale as a server; outbound MCP client version\nsupport is configured separately.\n\n`resources/list` pages through the effective native skill instruction catalog,\nand `resources/read` returns the accepted instruction body for an exact listed\n`codewhale://skills/...` URI. Discovery follows the configured skill roots,\nprecedence and workspace trust; disabled skills are omitted. Reads supply inert\nMarkdown and never activate a skill. These are instruction-body resources, not\noriginal SKILL.md files or complete skill packages. Plugin skills, arbitrary\nfilesystem paths and session transcripts are not exposed by this catalog.\nThe old workspace/session entries, which had no resource reader, are removed.\nResource templates are empty. Lists and reads use private caching with a zero\nTTL; restart pagination if a changed catalog invalidates a cursor.\n\nOnly tools and resources are advertised. Subscriptions, list-change\nnotifications, the Skills extension, Apps, tasks and interactive approval are\nnot implemented by this server. Tool execution remains serialized through the\nRust registry, and request cancellation reaches the existing tool context.\nKeep stdin open until responses arrive; closing it cancels outstanding work.\nMalformed JSON and oversized input frames are discarded by the bounded SDK\ntransport, while well-formed invalid JSON-RPC receives an error.\n\n## Setup wizard vs manual MCP setup (#3407)\n\nThe `/setup` hub includes an optional **Tools and MCP**\nstep. That step is discovery/readiness only:\n\n| Wizard can do | Still requires manual / explicit action |\n| --- | --- |\n| Show configured servers as `healthy` / `needs_config` / `off` | Start or connect MCP servers |\n| Report config path presence (global + project) | Write or edit `mcp.json` contents |\n| Safe static health probe (missing command/url, broken absolute path, missing bearer env) | `codewhale mcp validate`, live connect, OAuth login |\n| Point at safe on-ramps (`/mcp`, `codewhale mcp init`, `codewhale doctor`) | Install community skills, trust skills, enable plugins |\n| Share Hotbar source counts from the same skill/MCP adapters (#3399) | Bind Hotbar slots (Hotbar step / `H`) |\n| Record optional/`needs_action` setup_state without blocking first-run | Anything that spawns processes or installs packages |\n\nEmpty inventory is **not** an error: first-run users see “nothing configured\nyet, that’s fine.” Failing or incomplete configured servers surface as\n`needs_config` with an actionable hint and never block setup completion.\nEnumeration never executes MCP/plugin commands beyond the static probe.\nSummaries redact commands, args, env, headers, and tokens.\n\n`codewhale doctor` reports MCP/skills/tools/plugLine truncated
}
]
},
@@ -224,8 +224,8 @@
},
{
"path": "docs/CONFIGURATION.md",
"sha256": "3e076746af15d2c2f25bd60d86648c95ca17fc98ec0d9b1bd4d3a025958dd5f3",
"content": "# Configuration\n\ncodewhale reads configuration from a TOML file plus environment variables.\nAt process startup it may also load literal built-in-provider credentials from\na workspace-local `.env` file. Use the tracked `.env.example` as the template;\ncopy it to `.env`, then add only credential values.\n\nA workspace is not configuration authority. Codewhale therefore ignores\nconfig/profile/home paths, provider/model/base-URL routing, MCP/plugin state,\napproval/sandbox/shell posture, executable paths, runtime settings, and every\nother non-credential `.env` entry. Variable expansion is rejected so a\nrepository cannot substitute an ambient secret into a credential value. Use\n`config.toml`, CLI flags, or values exported by the launching shell for those\nexplicit control-plane settings. `.env` is read from a stable regular-file\nhandle, is capped at 1 MiB, and symbolic links, reparse points, and multiply\nlinked files are rejected.\n\n## Reading and checking configuration from the CLI\n\n`codewhale config get <key>` reads scalar keys, whole tables such as `tools`,\nand nested paths such as `tools.user_input_timeout_seconds`. Displayed tables\nand nested values apply the same recursive credential redaction as `config dump`.\n\n`config set` supports its named scalar keys and the provider, route, and\nnotification commands. It refuses a key that nothing reads and suggests the\nnearest real key (`config set calm_mod on` names `calm_mode`). A settings.toml\nkey such as `calm_mode` or `tool_collapse` is validated and written to the\nuser-global settings.toml, not config.toml; `config get` reads it back from\nthere even when an old config.toml copy (which nothing reads) is still present,\nand names that copy so you can `config unset` it. Settings keys have no project\nscope, so `--project` refuses them. A config.toml key is checked against the\ntype its reader expects and stored as a TOML boolean or number where the\nreader needs one (`yolo = true`, `max_subagents = 4`); `reasoning_effort`\naccepts the same aliases as `/effort` (#6563). Other dotted writes fail before\nmodifying the file and name the TOML table to edit. For example, set a tools\ntimeout in the file as:\n\n```toml\n[tools]\nuser_input_timeout_seconds = 0\n```\n\n`codewhale config doctor` checks credential presence and endpoint shape, and\nwarns about each config.toml root key that nothing reads: a settings.toml key\nleft in config.toml is named as misplaced, and any other unread key gets a\ndid-you-mean. Keys read by any runtime reader are not reported. The warnings do\nnot fail the check, and a clean result does not validate every runtime setting's\nvalue (#6083, #6563).\n\n## Constitution, project instructions, and repo authority\n\nCodewhale has several instruction surfaces. They are deliberately separate so a\npersonal constitution, repo policy, project instructions, and runtime security\ncontrols do not blur together.\n\n- **Bundled global Constitution** — the compiled base law in the binary. It is\n the default floor for every session.\n- **User-global constitution** — the normal guided setup output. Manage it with\n `/constitution` or `/setup`; Codewhale stores structured data at\n `$CODEWHALE_HOME/constitution.json` (default `~/.codewhale/constitution.json`)\n and renders it into a separate `<codewhale_user_constitution>` prose block.\n This can express preferences and stop conditions, but it does not change\n runtime approval policy, sandbox, shell, network, trust, or MCP permissions.\n- **Repo-local constitution** — optional project policy in\n `.codewhale/constitution.json`, described below.\n- **`AGENTS.md`** — cross-agent **project instructions** (prose). This is the\n canonical file for \"how should an agent work in this repo.\" Run `/init` to\n scaffold one. Instruction files written for other tools (`CLAUDE.md`,\n `.claude/instructions.md`, `.cursorrules`, `.github/copilot-instructions.md`\n and similar) are read only when you opt in with the top-level\n `project_instruction_imports` key, for example `[\"claude\"]`, `[\"cursor\"]` or\n `[\"all\"]` (see `config.example.toml`); Codewhale warns when it finds one it\n did not load.\n\n Codewhale reads one instruction file per directory, from the repository\n root down to the workspace, and puts your global instruction file (such as\n `~/.codewhale/AGENTS.md`) in front of them. The nearest scope reads last\n and wins: the workspace file overrides its parents, and project files\n override the global file. Global and project instructions are budgeted\n separately in estimated tokens (see [Instruction budget](#instruction-budget-instruction_budget-6526)\n below): a scope over its budget drops whole files from the broadest end\n first, and a single file larger than the remaining budget keeps its head\n with a marker that names the bytes and tokens dropped. A file keeps at most\n its first 100 KiB, and a file larger than `file_max_bytesLine truncated
"sha256": "c36db36c85ad3be7d11a01dbdf0c74a5641844a62169431db5988a127a34cd69",
"content": "# Configuration\n\ncodewhale reads configuration from a TOML file plus environment variables.\nAt process startup it may also load literal built-in-provider credentials from\na workspace-local `.env` file. Use the tracked `.env.example` as the template;\ncopy it to `.env`, then add only credential values.\n\nA workspace is not configuration authority. Codewhale therefore ignores\nconfig/profile/home paths, provider/model/base-URL routing, MCP/plugin state,\napproval/sandbox/shell posture, executable paths, runtime settings, and every\nother non-credential `.env` entry. Variable expansion is rejected so a\nrepository cannot substitute an ambient secret into a credential value. Use\n`config.toml`, CLI flags, or values exported by the launching shell for those\nexplicit control-plane settings. `.env` is read from a stable regular-file\nhandle, is capped at 1 MiB, and symbolic links, reparse points, and multiply\nlinked files are rejected.\n\n## Reading and checking configuration from the CLI\n\n`codewhale config get <key>` reads scalar keys, whole tables such as `tools`,\nand nested paths such as `tools.user_input_timeout_seconds`. Displayed tables\nand nested values apply the same recursive credential redaction as `config dump`.\n\n`config set` supports its named scalar keys and the provider, route, and\nnotification commands. It refuses a key that nothing reads and suggests the\nnearest real key (`config set calm_mod on` names `calm_mode`). A settings.toml\nkey such as `calm_mode` or `tool_collapse` is validated and written to the\nuser-global settings.toml, not config.toml; `config get` reads it back from\nthere even when an old config.toml copy (which nothing reads) is still present,\nand names that copy so you can `config unset` it. Settings keys have no project\nscope, so `--project` refuses them. A config.toml key is checked against the\ntype its reader expects and stored as a TOML boolean or number where the\nreader needs one (`yolo = true`, `max_subagents = 4`); `reasoning_effort`\naccepts the same aliases as `/effort` (#6563). Other dotted writes fail before\nmodifying the file and name the TOML table to edit. For example, set a tools\ntimeout in the file as:\n\n```toml\n[tools]\nuser_input_timeout_seconds = 0\n```\n\n`codewhale config doctor` checks credential presence and endpoint shape, and\nwarns about each config.toml root key that nothing reads: a settings.toml key\nleft in config.toml is named as misplaced, and any other unread key gets a\ndid-you-mean. Keys read by any runtime reader are not reported. The warnings do\nnot fail the check, and a clean result does not validate every runtime setting's\nvalue (#6083, #6563).\n\n## Constitution, project instructions, and repo authority\n\nCodewhale has several instruction surfaces. They are deliberately separate so a\npersonal constitution, repo policy, project instructions, and runtime security\ncontrols do not blur together.\n\n- **Bundled global Constitution** — the compiled base law in the binary. It is\n the default floor for every session.\n- **User-global constitution** — the normal guided setup output. Manage it with\n `/constitution` or `/setup`; Codewhale stores structured data at\n `$CODEWHALE_HOME/constitution.json` (default `~/.codewhale/constitution.json`)\n and renders it into a separate `<codewhale_user_constitution>` prose block.\n This can express preferences and stop conditions, but it does not change\n runtime approval policy, sandbox, shell, network, trust, or MCP permissions.\n- **Repo-local constitution** — optional project policy in\n `.codewhale/constitution.json`, described below.\n- **`AGENTS.md`** — cross-agent **project instructions** (prose). This is the\n canonical file for \"how should an agent work in this repo.\" Run `/init` to\n scaffold one. Instruction files written for other tools (`CLAUDE.md`,\n `.claude/instructions.md`, `.cursorrules`, `.github/copilot-instructions.md`\n and similar) are read only when you opt in with the top-level\n `project_instruction_imports` key, for example `[\"claude\"]`, `[\"cursor\"]` or\n `[\"all\"]` (see `config.example.toml`); Codewhale warns when it finds one it\n did not load.\n\n Codewhale reads one instruction file per directory, from the repository\n root down to the workspace, and puts your global instruction file (such as\n `~/.codewhale/AGENTS.md`) in front of them. The nearest scope reads last\n and wins: the workspace file overrides its parents, and project files\n override the global file. Global and project instructions are budgeted\n separately in estimated tokens (see [Instruction budget](#instruction-budget-instruction_budget-6526)\n below): a scope over its budget drops whole files from the broadest end\n first, and a single file larger than the remaining budget keeps its head\n with a marker that names the bytes and tokens dropped. A file keeps at most\n its first 100 KiB, and a file larger than `file_max_bytesLine truncated
}
]
},
@@ -256,8 +256,8 @@
},
{
"path": "docs/CONFIGURATION.md",
"sha256": "3e076746af15d2c2f25bd60d86648c95ca17fc98ec0d9b1bd4d3a025958dd5f3",
"content": "# Configuration\n\ncodewhale reads configuration from a TOML file plus environment variables.\nAt process startup it may also load literal built-in-provider credentials from\na workspace-local `.env` file. Use the tracked `.env.example` as the template;\ncopy it to `.env`, then add only credential values.\n\nA workspace is not configuration authority. Codewhale therefore ignores\nconfig/profile/home paths, provider/model/base-URL routing, MCP/plugin state,\napproval/sandbox/shell posture, executable paths, runtime settings, and every\nother non-credential `.env` entry. Variable expansion is rejected so a\nrepository cannot substitute an ambient secret into a credential value. Use\n`config.toml`, CLI flags, or values exported by the launching shell for those\nexplicit control-plane settings. `.env` is read from a stable regular-file\nhandle, is capped at 1 MiB, and symbolic links, reparse points, and multiply\nlinked files are rejected.\n\n## Reading and checking configuration from the CLI\n\n`codewhale config get <key>` reads scalar keys, whole tables such as `tools`,\nand nested paths such as `tools.user_input_timeout_seconds`. Displayed tables\nand nested values apply the same recursive credential redaction as `config dump`.\n\n`config set` supports its named scalar keys and the provider, route, and\nnotification commands. It refuses a key that nothing reads and suggests the\nnearest real key (`config set calm_mod on` names `calm_mode`). A settings.toml\nkey such as `calm_mode` or `tool_collapse` is validated and written to the\nuser-global settings.toml, not config.toml; `config get` reads it back from\nthere even when an old config.toml copy (which nothing reads) is still present,\nand names that copy so you can `config unset` it. Settings keys have no project\nscope, so `--project` refuses them. A config.toml key is checked against the\ntype its reader expects and stored as a TOML boolean or number where the\nreader needs one (`yolo = true`, `max_subagents = 4`); `reasoning_effort`\naccepts the same aliases as `/effort` (#6563). Other dotted writes fail before\nmodifying the file and name the TOML table to edit. For example, set a tools\ntimeout in the file as:\n\n```toml\n[tools]\nuser_input_timeout_seconds = 0\n```\n\n`codewhale config doctor` checks credential presence and endpoint shape, and\nwarns about each config.toml root key that nothing reads: a settings.toml key\nleft in config.toml is named as misplaced, and any other unread key gets a\ndid-you-mean. Keys read by any runtime reader are not reported. The warnings do\nnot fail the check, and a clean result does not validate every runtime setting's\nvalue (#6083, #6563).\n\n## Constitution, project instructions, and repo authority\n\nCodewhale has several instruction surfaces. They are deliberately separate so a\npersonal constitution, repo policy, project instructions, and runtime security\ncontrols do not blur together.\n\n- **Bundled global Constitution** — the compiled base law in the binary. It is\n the default floor for every session.\n- **User-global constitution** — the normal guided setup output. Manage it with\n `/constitution` or `/setup`; Codewhale stores structured data at\n `$CODEWHALE_HOME/constitution.json` (default `~/.codewhale/constitution.json`)\n and renders it into a separate `<codewhale_user_constitution>` prose block.\n This can express preferences and stop conditions, but it does not change\n runtime approval policy, sandbox, shell, network, trust, or MCP permissions.\n- **Repo-local constitution** — optional project policy in\n `.codewhale/constitution.json`, described below.\n- **`AGENTS.md`** — cross-agent **project instructions** (prose). This is the\n canonical file for \"how should an agent work in this repo.\" Run `/init` to\n scaffold one. Instruction files written for other tools (`CLAUDE.md`,\n `.claude/instructions.md`, `.cursorrules`, `.github/copilot-instructions.md`\n and similar) are read only when you opt in with the top-level\n `project_instruction_imports` key, for example `[\"claude\"]`, `[\"cursor\"]` or\n `[\"all\"]` (see `config.example.toml`); Codewhale warns when it finds one it\n did not load.\n\n Codewhale reads one instruction file per directory, from the repository\n root down to the workspace, and puts your global instruction file (such as\n `~/.codewhale/AGENTS.md`) in front of them. The nearest scope reads last\n and wins: the workspace file overrides its parents, and project files\n override the global file. Global and project instructions are budgeted\n separately in estimated tokens (see [Instruction budget](#instruction-budget-instruction_budget-6526)\n below): a scope over its budget drops whole files from the broadest end\n first, and a single file larger than the remaining budget keeps its head\n with a marker that names the bytes and tokens dropped. A file keeps at most\n its first 100 KiB, and a file larger than `file_max_bytesLine truncated
"sha256": "c36db36c85ad3be7d11a01dbdf0c74a5641844a62169431db5988a127a34cd69",
"content": "# Configuration\n\ncodewhale reads configuration from a TOML file plus environment variables.\nAt process startup it may also load literal built-in-provider credentials from\na workspace-local `.env` file. Use the tracked `.env.example` as the template;\ncopy it to `.env`, then add only credential values.\n\nA workspace is not configuration authority. Codewhale therefore ignores\nconfig/profile/home paths, provider/model/base-URL routing, MCP/plugin state,\napproval/sandbox/shell posture, executable paths, runtime settings, and every\nother non-credential `.env` entry. Variable expansion is rejected so a\nrepository cannot substitute an ambient secret into a credential value. Use\n`config.toml`, CLI flags, or values exported by the launching shell for those\nexplicit control-plane settings. `.env` is read from a stable regular-file\nhandle, is capped at 1 MiB, and symbolic links, reparse points, and multiply\nlinked files are rejected.\n\n## Reading and checking configuration from the CLI\n\n`codewhale config get <key>` reads scalar keys, whole tables such as `tools`,\nand nested paths such as `tools.user_input_timeout_seconds`. Displayed tables\nand nested values apply the same recursive credential redaction as `config dump`.\n\n`config set` supports its named scalar keys and the provider, route, and\nnotification commands. It refuses a key that nothing reads and suggests the\nnearest real key (`config set calm_mod on` names `calm_mode`). A settings.toml\nkey such as `calm_mode` or `tool_collapse` is validated and written to the\nuser-global settings.toml, not config.toml; `config get` reads it back from\nthere even when an old config.toml copy (which nothing reads) is still present,\nand names that copy so you can `config unset` it. Settings keys have no project\nscope, so `--project` refuses them. A config.toml key is checked against the\ntype its reader expects and stored as a TOML boolean or number where the\nreader needs one (`yolo = true`, `max_subagents = 4`); `reasoning_effort`\naccepts the same aliases as `/effort` (#6563). Other dotted writes fail before\nmodifying the file and name the TOML table to edit. For example, set a tools\ntimeout in the file as:\n\n```toml\n[tools]\nuser_input_timeout_seconds = 0\n```\n\n`codewhale config doctor` checks credential presence and endpoint shape, and\nwarns about each config.toml root key that nothing reads: a settings.toml key\nleft in config.toml is named as misplaced, and any other unread key gets a\ndid-you-mean. Keys read by any runtime reader are not reported. The warnings do\nnot fail the check, and a clean result does not validate every runtime setting's\nvalue (#6083, #6563).\n\n## Constitution, project instructions, and repo authority\n\nCodewhale has several instruction surfaces. They are deliberately separate so a\npersonal constitution, repo policy, project instructions, and runtime security\ncontrols do not blur together.\n\n- **Bundled global Constitution** — the compiled base law in the binary. It is\n the default floor for every session.\n- **User-global constitution** — the normal guided setup output. Manage it with\n `/constitution` or `/setup`; Codewhale stores structured data at\n `$CODEWHALE_HOME/constitution.json` (default `~/.codewhale/constitution.json`)\n and renders it into a separate `<codewhale_user_constitution>` prose block.\n This can express preferences and stop conditions, but it does not change\n runtime approval policy, sandbox, shell, network, trust, or MCP permissions.\n- **Repo-local constitution** — optional project policy in\n `.codewhale/constitution.json`, described below.\n- **`AGENTS.md`** — cross-agent **project instructions** (prose). This is the\n canonical file for \"how should an agent work in this repo.\" Run `/init` to\n scaffold one. Instruction files written for other tools (`CLAUDE.md`,\n `.claude/instructions.md`, `.cursorrules`, `.github/copilot-instructions.md`\n and similar) are read only when you opt in with the top-level\n `project_instruction_imports` key, for example `[\"claude\"]`, `[\"cursor\"]` or\n `[\"all\"]` (see `config.example.toml`); Codewhale warns when it finds one it\n did not load.\n\n Codewhale reads one instruction file per directory, from the repository\n root down to the workspace, and puts your global instruction file (such as\n `~/.codewhale/AGENTS.md`) in front of them. The nearest scope reads last\n and wins: the workspace file overrides its parents, and project files\n override the global file. Global and project instructions are budgeted\n separately in estimated tokens (see [Instruction budget](#instruction-budget-instruction_budget-6526)\n below): a scope over its budget drops whole files from the broadest end\n first, and a single file larger than the remaining budget keeps its head\n with a marker that names the bytes and tokens dropped. A file keeps at most\n its first 100 KiB, and a file larger than `file_max_bytesLine truncated
}
]
},
@@ -277,8 +277,8 @@
},
{
"path": "docs/EXTENSIONS.md",
"sha256": "5bf540a2a1a1740e3ae251cfed8eb7066e03675934b4cdb57a9dd12a4ac48717",
"content": "# Writing an extension tool or command\n\nThe experimental TypeScript extension host runs reviewed plugin code in a\nshared Node process (or, as an opt-in, Bun). Rust still owns sessions, tool admission, approval and\nexecution. Extensions currently contribute tools and slash commands; they\ncannot provide an approval service, run a second agent loop or replace\nbuilt-in tools or commands.\n\n## Quick start\n\nStart with [hello-extension](examples/plugins/hello-extension/hello.mts) and its\n[plugin.json](examples/plugins/hello-extension/plugin.json). The example is\nexecuted unchanged by the host tests. It returns a greeting and does not read\nfiles or use the network.\n\n1. Explicitly enable `[features] extension_host = true` in your configuration,\n or start Codewhale with `codewhale --enable extension_host` (the flag goes\n before any subcommand).\n2. Install the example's directory using `/plugin install <local-directory>`.\n3. Run `/plugin validate hello-extension`, then `/plugin show hello-extension`.\n4. Run `/plugin enable hello-extension` to open its content/capability review.\n Read the review and personally run its exact `/plugin trust <token>` command,\n then enable the plugin. Trust alone does not execute the code.\n5. Ask Codewhale to use `hello_greet`. The tool follows the normal approval gate.\n6. Run `/hello-greet Codewhale`. The command runs when you type it, and shows\n its answer in the transcript with its origin, `extension:hello-extension`.\n\nAn authoring agent should stop after installation and validation and present\nthe review to the person. Do not trust or enable a bundle automatically.\n\n## Locations, installation and trust\n\nUser bundles live under `~/.codewhale/plugins/`; workspace bundles live under\n`<workspace>/.codewhale/plugins/`. New bundles use Agent Plugins v1.0.0\n`plugin.json`. Declare `extensions[\"net.codewhale\"].native.path` as one regular\n`.mjs`, `.js` or `.mts` file inside the bundle. Directories and other extensions\nfail validation when the host is enabled. With the flag off, native entries\nremain inventory-only.\n\nA plugin may split its code across several entries with `native.paths` (up to\n64, alone or beside `path`). They are activated in the order declared, as the\nfibers of one plugin: they share one owner, so one disable, review change or\ncrash tears all of them down together. Each entry is a separate module with its\nown `apply`, and tool and command names must be unique across them. If an entry\nfails to activate (throws, requires a service the host does not provide, or has\na registration refused), only that entry is withdrawn: entries that already\nactivated stay registered, later entries still activate, and the plugin fails\nonly when no entry activates. Listing the same file twice is one entry.\n\n## Runtime\n\n`[extension_host] runtime` selects what runs the host. Node is the default.\nBun is an opt-in: it is qualified on macOS only, and becomes the default only\nafter an explicit, recorded cutover.\n\n```toml\n[extension_host]\nruntime = \"node\" # default: Node only\n# runtime = \"bun\" # Bun only; fails rather than falling back to Node\n# runtime = \"auto\" # Bun >= 1.4.0 if one is found and starts, otherwise Node\n# bun = \"~/.bun/bin/bun\" # the only Bun tried when set\n# node = \"/opt/homebrew/bin/node\" # the only Node tried when set\n# mcp_backend = \"rust\" # temporary opt-out; default is \"host\"\n```\n\nThe default `mcp_backend = \"host\"` uses the pinned MCP SDK independently of\n`[features] extension_host`, which controls optional Native extensions. Existing\nstdio and HTTP servers use modern discovery when supported; explicitly configured\nlegacy SSE servers use their legacy handshake. Rust retains process and network\nauthority, credential resolution, catalog admission, tool approval and cancellation.\nThe SDK receives opaque session selectors and exact operation grants. Modern\nrequests carry Rust-approved protocol metadata and headers without a session id.\n\nA typed discovery refusal can admit a legacy handshake. Only an explicit HTTP\ntransport refusal during that handshake can admit legacy SSE with a fresh exact\nRust grant. Legacy HTTP retains its GET preflight and bounded OAuth refresh;\nmodern HTTP does not inherit those session or resend behaviors. Incomplete result\nflows are reported without automatically repeating the operation. Credentials,\nconfigured URLs, OAuth browser/login operations and Computer Use decision keys\nstay in Rust.\n\nBoth backends share recorded semantic MCP goldens and retain separate transport\ngoldens, including discovery, authentication and session evidence. The Host default\nrequires those fixtures and the actual broker acceptance checks, plus the platform\nisolation and measured gates in Ops CURRENT_DECISIONS §26. An explicit\n`mcp_backend = \"rust\"` remains available for one release after the qualified default\nflip, before the native protocoLine truncated
"sha256": "2f8c5d104925bd9fcc513aea21e1f546f46cbe9a98f25e1f5bceaf346fa3886f",
"content": "# Writing an extension tool or command\n\nThe experimental TypeScript extension host runs reviewed plugin code in a\nshared Node process (or, as an opt-in, Bun). Rust still owns sessions, tool admission, approval and\nexecution. Extensions currently contribute tools and slash commands; they\ncannot provide an approval service, run a second agent loop or replace\nbuilt-in tools or commands.\n\n## Quick start\n\nStart with [hello-extension](examples/plugins/hello-extension/hello.mts) and its\n[plugin.json](examples/plugins/hello-extension/plugin.json). The example is\nexecuted unchanged by the host tests. It returns a greeting and does not read\nfiles or use the network.\n\n1. Explicitly enable `[features] extension_host = true` in your configuration,\n or start Codewhale with `codewhale --enable extension_host` (the flag goes\n before any subcommand).\n2. Install the example's directory using `/plugin install <local-directory>`.\n3. Run `/plugin validate hello-extension`, then `/plugin show hello-extension`.\n4. Run `/plugin enable hello-extension` to open its content/capability review.\n Read the review and personally run its exact `/plugin trust <token>` command,\n then enable the plugin. Trust alone does not execute the code.\n5. Ask Codewhale to use `hello_greet`. The tool follows the normal approval gate.\n6. Run `/hello-greet Codewhale`. The command runs when you type it, and shows\n its answer in the transcript with its origin, `extension:hello-extension`.\n\nAn authoring agent should stop after installation and validation and present\nthe review to the person. Do not trust or enable a bundle automatically.\n\n## Locations, installation and trust\n\nUser bundles live under `~/.codewhale/plugins/`; workspace bundles live under\n`<workspace>/.codewhale/plugins/`. New bundles use Agent Plugins v1.0.0\n`plugin.json`. Declare `extensions[\"net.codewhale\"].native.path` as one regular\n`.mjs`, `.js` or `.mts` file inside the bundle. Directories and other extensions\nfail validation when the host is enabled. With the flag off, native entries\nremain inventory-only.\n\nA plugin may split its code across several entries with `native.paths` (up to\n64, alone or beside `path`). They are activated in the order declared, as the\nfibers of one plugin: they share one owner, so one disable, review change or\ncrash tears all of them down together. Each entry is a separate module with its\nown `apply`, and tool and command names must be unique across them. If an entry\nfails to activate (throws, requires a service the host does not provide, or has\na registration refused), only that entry is withdrawn: entries that already\nactivated stay registered, later entries still activate, and the plugin fails\nonly when no entry activates. Listing the same file twice is one entry.\n\n## Runtime\n\n`[extension_host] runtime` selects what runs the host. Node is the default.\nBun is an opt-in: it is qualified on macOS only, and becomes the default only\nafter an explicit, recorded cutover.\n\n```toml\n[extension_host]\nruntime = \"node\" # default: Node only\n# runtime = \"bun\" # Bun only; fails rather than falling back to Node\n# runtime = \"auto\" # Bun >= 1.4.0 if one is found and starts, otherwise Node\n# bun = \"~/.bun/bin/bun\" # the only Bun tried when set\n# node = \"/opt/homebrew/bin/node\" # the only Node tried when set\n# mcp_backend = \"rust\" # temporary opt-out; default is \"host\"\n```\n\nThe default `mcp_backend = \"host\"` uses the pinned MCP SDK independently of\n`[features] extension_host`, which controls optional Native extensions. Existing\nstdio and HTTP servers use modern discovery when supported; explicitly configured\nlegacy SSE servers use their legacy handshake. Rust retains process and network\nauthority, credential resolution, catalog admission, tool approval and cancellation.\nThe SDK receives opaque session selectors and exact operation grants. Modern\nrequests carry Rust-approved protocol metadata and headers without a session id.\n\nA typed discovery refusal can admit a legacy handshake. Only an explicit HTTP\ntransport refusal during that handshake can admit legacy SSE with a fresh exact\nRust grant. Legacy HTTP retains its GET preflight and bounded OAuth refresh;\nmodern HTTP does not inherit those session or resend behaviors. Incomplete result\nflows are reported without automatically repeating the operation. Credentials,\nconfigured URLs, OAuth browser/login operations and Computer Use decision keys\nstay in Rust.\n\nBoth backends share recorded semantic MCP goldens and retain separate transport\ngoldens, including discovery, authentication and session evidence. The Host default\nrequires those fixtures and the actual broker acceptance checks, plus the platform\nisolation and measured gates in Ops CURRENT_DECISIONS §26. An explicit\n`mcp_backend = \"rust\"` remains available for one release after the qualified default\nflip, before the native protocoLine truncated
}
]
},
@@ -288,8 +288,8 @@
"sources": [
{
"path": "docs/RUNTIME_API.md",
"sha256": "dbb3beebdb8395dd7ccf9fb125d9a767cd404b0565fd36b6131c7f5f0e501268",
"content": "# Codewhale Engine Runtime API & Integration Contract\n\n> 阅读简体中文版:[zh_hans/RUNTIME_API.md](zh_hans/RUNTIME_API.md)。\n\n`codewhale app-server --http` exposes the [Codewhale Engine](ARCHITECTURE.md) through\nits canonical local Runtime API and control plane.\nLocal SDKs, mobile/remote-control clients, and editor integrations talk to it\ninstead of screen-scraping terminal output. It serves the full HTTP/SSE runtime\nAPI (`/v1/*`), a JSON-RPC control transport over stdio, and the phone-friendly\nmobile page. `codewhale doctor --json` provides machine-readable health, and\n`codewhale serve --acp` speaks the Agent Client Protocol over stdio for editors\nsuch as Zed.\n\n`codewhale serve --http` / `serve --mobile` remain as **compatibility aliases**\nfor `codewhale app-server --http` / `--mobile`; both launch the identical\nserver. Desktop, web, SDK and custom integrations should target\n`app-server --http`. Bare `app-server`, `--stdio` and `--socket` select smaller\ncompatibility interfaces, not the full Engine API.\n\n`codewhale exec` is the separate one-shot headless worker path (stream-json,\nfleet worker subprocess, CI primitive). It is not part of this API, but it\nshares the same runtime, provider/model resolution, permission profiles, and\nevent vocabulary.\n\nThis document is the stable integration contract for native workbench\napplications (and other local supervisors) that embed the Codewhale Engine.\n\n## Architecture\n\n```\nlocal supervisor / SDK / automation harness\n │\n ├─ codewhale app-server --http → HTTP/SSE runtime API (/v1/*) [canonical]\n ├─ codewhale app-server --mobile → runtime API + mobile control page\n ├─ codewhale app-server --stdio → JSON-RPC control transport over stdio\n ├─ codewhale app-server --socket → same JSON-RPC over a unix domain socket (desktop daemon)\n ├─ codewhale doctor --json → machine-readable health & capability\n ├─ codewhale serve --acp → ACP stdio agent for editors such as Zed\n ├─ codewhale serve --mcp → MCP stdio server\n ├─ codewhale serve --http/--mobile → legacy aliases for `app-server --http/--mobile`\n └─ codewhale exec [args] → one-shot headless worker (stream-json)\n```\n\nThe engine runs as a local-only process. All APIs bind to `localhost` by\ndefault. No hosted relay, no provider-token custody, no secret leakage.\n\nFor the read-only record of what a thread or turn did, see\n[`docs/RECEIPTS.md`](RECEIPTS.md): `codewhale receipts` on the CLI and the\n`/receipt` routes under **Threads** below.\n\n## Runtime API entrypoints\n\n| Entry | Transport | Use |\n|---|---|---|\n| `codewhale web [--port 7878]` | HTTP/SSE on `127.0.0.1:7878` + embedded client | First-class loopback-only browser client; opens the default browser |\n| `codewhale app-server --http` | HTTP/SSE on `127.0.0.1:7878` | Full `/v1/*` runtime API (canonical) |\n| `codewhale app-server --mobile` | HTTP/SSE on loopback + `/mobile` | Runtime API + local mobile control page |\n| `codewhale app-server --stdio` | JSON-RPC 2.0 over stdio | Compatibility controls and explicit tool-free turns; no full Engine event or approval/input channel. |\n| `codewhale app-server --socket [--socket-path P]` | JSON-RPC 2.0 over a `0600` Unix domain socket (macOS/Linux) or an owner-only named pipe (Windows) | Desktop daemon: multi-client, kernel peer identity checked, `daemon/attach` claim handshake. Windows native-app qualification remains open. |\n| `codewhale app-server` | HTTP on `127.0.0.1:8787` | Compatibility `/healthz`, `/thread`, `/app`, `/prompt`. Turn requests require explicit `allowed_tools: []`; interactive execution fails before a turn starts. Use `app-server --http` for tools, approvals, clarification, goal execution, steering and the full event journal. |\n| `codewhale serve --http` / `--mobile` | same server as `app-server --http`/`--mobile` | Compatibility aliases |\n\n`app-server --http` and `--mobile` launch the same mature runtime API server\nhistorically reached through `serve --http` — no routes or behavior changed, so\nevery endpoint documented below is identical across both entrypoints. The\nruntime API token is read from `--auth-token`, then `CODEWHALE_RUNTIME_TOKEN`,\nthen `DEEPSEEK_RUNTIME_TOKEN`; use `--insecure-no-auth` only with a loopback\nbind. The `serve` compatibility aliases keep their `--insecure` flag.\nThe legacy in-process `codewhale app-server` also requires an explicit\n`--auth-token` or `CODEWHALE_APP_SERVER_TOKEN` before binding a non-loopback\nhost; its generated one-time `cwapp_*` token is loopback-only.\n\nDevice tokens minted by the master through `POST /v1/auth/client-tokens`\nhave an immutable `intent`: `watch` (the default when omitted) or explicit\n`drive`. Labels do not grant authority. Watch permits ordinary GET/HEAD reads,\nComputer displayLine truncated
"sha256": "7380f415b65e7115930f9108424c89ed7cc849a57622cbf32520225dac7ad27c",
"content": "# Codewhale Engine Runtime API & Integration Contract\n\n> 阅读简体中文版:[zh_hans/RUNTIME_API.md](zh_hans/RUNTIME_API.md)。\n\n`codewhale app-server --http` exposes the [Codewhale Engine](ARCHITECTURE.md) through\nits canonical local Runtime API and control plane.\nLocal SDKs, mobile/remote-control clients, and editor integrations talk to it\ninstead of screen-scraping terminal output. It serves the full HTTP/SSE runtime\nAPI (`/v1/*`), a JSON-RPC control transport over stdio, and the phone-friendly\nmobile page. `codewhale doctor --json` provides machine-readable health, and\n`codewhale serve --acp` speaks the Agent Client Protocol over stdio for editors\nsuch as Zed.\n\n`codewhale serve --http` / `serve --mobile` remain as **compatibility aliases**\nfor `codewhale app-server --http` / `--mobile`; both launch the identical\nserver. Desktop, web, SDK and custom integrations should target\n`app-server --http`. Bare `app-server`, `--stdio` and `--socket` select smaller\ncompatibility interfaces, not the full Engine API.\n\n`codewhale exec` is the separate one-shot headless worker path (stream-json,\nfleet worker subprocess, CI primitive). It is not part of this API, but it\nshares the same runtime, provider/model resolution, permission profiles, and\nevent vocabulary.\n\nThis document is the stable integration contract for native workbench\napplications (and other local supervisors) that embed the Codewhale Engine.\n\n## Architecture\n\n```\nlocal supervisor / SDK / automation harness\n │\n ├─ codewhale app-server --http → HTTP/SSE runtime API (/v1/*) [canonical]\n ├─ codewhale app-server --mobile → runtime API + mobile control page\n ├─ codewhale app-server --stdio → JSON-RPC control transport over stdio\n ├─ codewhale app-server --socket → same JSON-RPC over a unix domain socket (desktop daemon)\n ├─ codewhale doctor --json → machine-readable health & capability\n ├─ codewhale serve --acp → ACP stdio agent for editors such as Zed\n ├─ codewhale serve --mcp → MCP stdio server\n ├─ codewhale serve --http/--mobile → legacy aliases for `app-server --http/--mobile`\n └─ codewhale exec [args] → one-shot headless worker (stream-json)\n```\n\nThe engine runs as a local-only process. All APIs bind to `localhost` by\ndefault. No hosted relay, no provider-token custody, no secret leakage.\n\nFor the read-only record of what a thread or turn did, see\n[`docs/RECEIPTS.md`](RECEIPTS.md): `codewhale receipts` on the CLI and the\n`/receipt` routes under **Threads** below.\n\n## Runtime API entrypoints\n\n| Entry | Transport | Use |\n|---|---|---|\n| `codewhale web [--port 7878]` | HTTP/SSE on `127.0.0.1:7878` + embedded client | First-class loopback-only browser client; opens the default browser |\n| `codewhale app-server --http` | HTTP/SSE on `127.0.0.1:7878` | Full `/v1/*` runtime API (canonical) |\n| `codewhale app-server --mobile` | HTTP/SSE on loopback + `/mobile` | Runtime API + local mobile control page |\n| `codewhale app-server --stdio` | JSON-RPC 2.0 over stdio | Compatibility controls and explicit tool-free turns; no full Engine event or approval/input channel. |\n| `codewhale app-server --socket [--socket-path P]` | JSON-RPC 2.0 over a `0600` Unix domain socket (macOS/Linux) or an owner-only named pipe (Windows) | Desktop daemon: multi-client, kernel peer identity checked, `daemon/attach` claim handshake. Windows native-app qualification remains open. |\n| `codewhale app-server` | HTTP on `127.0.0.1:8787` | Compatibility `/healthz`, `/thread`, `/app`, `/prompt`. Turn requests require explicit `allowed_tools: []`; interactive execution fails before a turn starts. Use `app-server --http` for tools, approvals, clarification, goal execution, steering and the full event journal. |\n| `codewhale serve --http` / `--mobile` | same server as `app-server --http`/`--mobile` | Compatibility aliases |\n\n`app-server --http` and `--mobile` launch the same mature runtime API server\nhistorically reached through `serve --http` — no routes or behavior changed, so\nevery endpoint documented below is identical across both entrypoints. The\nruntime API token is read from `--auth-token`, then `CODEWHALE_RUNTIME_TOKEN`,\nthen `DEEPSEEK_RUNTIME_TOKEN`; use `--insecure-no-auth` only with a loopback\nbind. The `serve` compatibility aliases keep their `--insecure` flag.\nThe legacy in-process `codewhale app-server` also requires an explicit\n`--auth-token` or `CODEWHALE_APP_SERVER_TOKEN` before binding a non-loopback\nhost; its generated one-time `cwapp_*` token is loopback-only.\n\nDevice tokens minted by the master through `POST /v1/auth/client-tokens`\nhave an immutable `intent`: `watch` (the default when omitted) or explicit\n`drive`. Labels do not grant authority. Watch permits ordinary GET/HEAD reads,\nComputer displayLine truncated
}
]
},
@@ -326,8 +326,8 @@
"sources": [
{
"path": "CONTRIBUTING.md",
"sha256": "e6ce0e044f2e61f290fdffee83879e965b4c6fc40f8d61f9f2b2037e692cfa79",
"content": "# Contributing to codewhale\n\nThank you for your interest in contributing to codewhale! This document provides guidelines and instructions for contributing.\n\n## Getting Started\n\n### Prerequisites\n\n- Rust 1.89 or later (edition 2024)\n- Cargo package manager\n- Git\n\n### Setting Up Development Environment\n\n1. Fork and clone the repository:\n ```bash\n git clone https://github.com/YOUR_USERNAME/CodeWhale.git\n cd CodeWhale\n ```\n\n2. Build the current engine and terminal:\n ```bash\n CODEWHALE_BUILD_SHA=\"$(git rev-parse HEAD)\" cargo build --locked -p codewhale-cli\n ```\n\n3. Run the tests near your change (see [Fast local loop](#fast-local-loop)):\n ```bash\n scripts/dev-test.sh tui your_test_filter\n ```\n\n4. Run with development settings:\n ```bash\n ./target/debug/codewhale --version\n ./target/debug/codewhale\n ```\n\n### Testing the latest source\n\nThe canonical source is [`codewhale-hq/Codewhale`'s `main` branch](https://github.com/codewhale-hq/Codewhale/tree/main).\nRelease candidates land there after their CI gates pass, so contributors can\ntest and build on the same source. Tagged downloads remain the latest published\nrelease; their version can lag the development version on `main`.\n\nFor a fresh checkout of the current source:\n\n```bash\ngit clone --branch main https://github.com/codewhale-hq/Codewhale.git\ncd Codewhale\nCODEWHALE_BUILD_SHA=\"$(git rev-parse HEAD)\" cargo build --release --locked -p codewhale-cli\n./target/release/codewhale --version\n./target/release/codewhale\n```\n\nInclude the commit shown by `--version` when reporting a problem. If you work\nfrom a fork, add the canonical repository as `upstream` and fetch `upstream/main`\nbefore starting a change; preserve any local work when updating your branch.\n\n## Development Workflow\n\n### Code Style\n\n- Run `cargo fmt` before committing to ensure consistent formatting\n- Run `cargo clippy` and address all warnings\n- Follow Rust naming conventions (snake_case for functions/variables, CamelCase for types)\n- Add documentation comments for public APIs\n\n### Testing\n\n- Write tests for new functionality\n- Run the tests near your change (see [Fast local loop](#fast-local-loop));\n your pull request runs the Linux workspace suite and a Windows build for\n you (see [What happens after you open a pull request](#what-happens-after-you-open-a-pull-request))\n- Colocate unit tests beside the code they cover (standard Rust `#[cfg(test)]`\n modules), and add integration tests under the owning crate's `tests/`\n directory (for example `crates/tui/tests/` or `crates/state/tests/`). The\n repository root `tests/` directory is not used\n\n### Pre-push verification\n\nThese are the Rust commands your pull request runs on Linux (the `Lint` and\n`Test (ubuntu-latest)` checks). You do not need all of them before every\npush: run `cargo fmt`, then check and test the crates you touched (see\n[Fast local loop](#fast-local-loop)). Run the full set locally when a change\nspans many crates, or let the pull request run it for you:\n\n```bash\ncargo fmt --all -- --check\ncargo clippy --workspace --all-targets --all-features --locked -- \\\n -D warnings \\\n -A clippy::uninlined_format_args \\\n -A clippy::too_many_arguments \\\n -A clippy::unnecessary_map_or\ncargo test --workspace --all-features --locked\n```\n\nClippy lints test, bench and example targets too (`--all-targets`), and the\nthree allowances above are the only ones CI and the release lane accept.\n\n#### Fast local loop\n\nThe full gate above is what CI enforces, but you do not need it for every\nedit. `crates/tui` is a ~750k-line crate, so the loop that stays fast is\nthe one that avoids rebuilding it more than necessary (numbers and the\nreasoning are in [`docs/BUILD_PERFORMANCE.md`](docs/BUILD_PERFORMANCE.md)):\n\n```bash\n# 1. Type-check first (seconds after the first build; no codegen, no link).\nscripts/dev-cargo.sh check -p codewhale-tui\n\n# 2. Run only the tests near your change (one crate, one filter).\nscripts/dev-test.sh tui fleet_setup\n# or: scripts/dev-test.sh crates/command-contract/src/elapsed.rs\n\n# 3. Run a whole crate's unit suite. scripts/dev-test.sh uses nextest when\n# it is installed (one process per test, all cores busy, slow tests\n# named; ~100 s here vs ~270 s with libtest).\ncargo install cargo-nextest --locked # once\nscripts/dev-test.sh tui\nscripts/dev-cargo.sh nextest run --workspace --all-features --locked\n\n# 4. The Linux suite your PR runs:\ncargo test --workspace --all-features --locked\n```\n\n`.config/nextest.toml` already serializes the PTY suite and bounds the\nintegration tests that spawn the real binary, so `cargo nextest run` is\nsafe to use on the whole workspace (nextest does not run doctests; the\nauthoritative `cargo test` gate does). Tests must not depend on running in\nthe same process as another test (nextest gives every test its own\nprocess); if a test Line truncated
"sha256": "5a13b836a4e56ce49672daf297abe93476fdc95316ccc54bf6c43d516b15e83a",
"content": "# Contributing to codewhale\n\nThank you for your interest in contributing to codewhale! This document provides guidelines and instructions for contributing.\n\n## Getting Started\n\n### Prerequisites\n\n- Rust 1.89 or later (edition 2024)\n- Cargo package manager\n- Git\n\n### Setting Up Development Environment\n\n1. Fork and clone the repository:\n ```bash\n git clone https://github.com/YOUR_USERNAME/CodeWhale.git\n cd CodeWhale\n ```\n\n2. Build the current engine and terminal:\n ```bash\n CODEWHALE_BUILD_SHA=\"$(git rev-parse HEAD)\" cargo build --locked -p codewhale-cli\n ```\n\n3. Run the tests near your change (see [Fast local loop](#fast-local-loop)):\n ```bash\n scripts/dev-test.sh tui your_test_filter\n ```\n\n4. Run with development settings:\n ```bash\n ./target/debug/codewhale --version\n ./target/debug/codewhale\n ```\n\n### Testing the latest source\n\nThe canonical source is [`codewhale-hq/Codewhale`'s `main` branch](https://github.com/codewhale-hq/Codewhale/tree/main).\nRelease candidates land there after their CI gates pass, so contributors can\ntest and build on the same source. Tagged downloads remain the latest published\nrelease; their version can lag the development version on `main`.\n\nFor a fresh checkout of the current source:\n\n```bash\ngit clone --branch main https://github.com/codewhale-hq/Codewhale.git\ncd Codewhale\nCODEWHALE_BUILD_SHA=\"$(git rev-parse HEAD)\" cargo build --release --locked -p codewhale-cli\n./target/release/codewhale --version\n./target/release/codewhale\n```\n\nInclude the commit shown by `--version` when reporting a problem. If you work\nfrom a fork, add the canonical repository as `upstream` and fetch `upstream/main`\nbefore starting a change; preserve any local work when updating your branch.\n\n## Development Workflow\n\n### Code Style\n\n- Run `cargo fmt` before committing to ensure consistent formatting\n- Run `cargo clippy` and address all warnings\n- Follow Rust naming conventions (snake_case for functions/variables, CamelCase for types)\n- Add documentation comments for public APIs\n\n### Testing\n\n- Write tests for new functionality\n- Run the tests near your change (see [Fast local loop](#fast-local-loop));\n your pull request runs the Linux workspace suite and a Windows build for\n you (see [What happens after you open a pull request](#what-happens-after-you-open-a-pull-request))\n- Colocate unit tests beside the code they cover (standard Rust `#[cfg(test)]`\n modules), and add integration tests under the owning crate's `tests/`\n directory (for example `crates/tui/tests/` or `crates/state/tests/`). The\n repository root `tests/` directory is not used\n\n### Pre-push verification\n\nThese are the Rust commands your pull request runs on Linux (the `Lint` and\n`Test (ubuntu-latest)` checks). You do not need all of them before every\npush: run `cargo fmt`, then check and test the crates you touched (see\n[Fast local loop](#fast-local-loop)). Run the full set locally when a change\nspans many crates, or let the pull request run it for you:\n\n```bash\ncargo fmt --all -- --check\ncargo clippy --workspace --all-targets --all-features --locked -- \\\n -D warnings \\\n -A clippy::uninlined_format_args \\\n -A clippy::too_many_arguments \\\n -A clippy::unnecessary_map_or\ncargo test --workspace --all-features --locked\n```\n\nClippy lints test, bench and example targets too (`--all-targets`), and the\nthree allowances above are the only ones CI and the release lane accept.\n\n#### Fast local loop\n\nThe full gate above is what CI enforces, but you do not need it for every\nedit. `crates/tui` is a ~750k-line crate, so the loop that stays fast is\nthe one that avoids rebuilding it more than necessary (numbers and the\nreasoning are in [`docs/BUILD_PERFORMANCE.md`](docs/BUILD_PERFORMANCE.md)):\n\n```bash\n# 1. Type-check first (seconds after the first build; no codegen, no link).\nscripts/dev-cargo.sh check -p codewhale-tui\n\n# 2. Run only the tests near your change (one crate, one filter).\nscripts/dev-test.sh tui fleet_setup\n# or: scripts/dev-test.sh crates/command-contract/src/elapsed.rs\n\n# 3. Run a whole crate's unit suite. scripts/dev-test.sh uses nextest when\n# it is installed (one process per test, all cores busy, slow tests\n# named; ~100 s here vs ~270 s with libtest).\ncargo install cargo-nextest --locked # once\nscripts/dev-test.sh tui\nscripts/dev-cargo.sh nextest run --workspace --all-features --locked\n\n# 4. The Linux suite your PR runs:\ncargo test --workspace --all-features --locked\n```\n\n`.config/nextest.toml` already serializes the PTY suite and bounds the\nintegration tests that spawn the real binary, so `cargo nextest run` is\nsafe to use on the whole workspace (nextest does not run doctests; the\nauthoritative `cargo test` gate does). Tests must not depend on running in\nthe same process as another test (nextest gives every test its own\nprocess); if a test Line truncated
},
{
"path": "docs/AGENT_ETHOS.md",
@@ -336,8 +336,8 @@
},
{
"path": "docs/CONTRIBUTORS.md",
"sha256": "2a0a80012465c19a3d2ef1d3aeb48547381c9cd965379db21950dfe0c3aa11cc",
"content": "# Contributors\n\nCodewhale is built in the open with a growing community of contributors. Every\nissue report and pull request is real project work — welcome at any experience\nlevel. This is the full per-PR contributor record, grouped into release/time\nbands with the newest band first so it stays scannable. Expand any band to see\neveryone.\n\nFor the live list, see the\n[GitHub contributors page](https://github.com/codewhale-hq/CodeWhale/graphs/contributors),\n[`AUTHOR_MAP`](https://github.com/codewhale-hq/CodeWhale/blob/main/.github/AUTHOR_MAP),\nand [CHANGELOG.md](../CHANGELOG.md).\n\n<!-- contributor-wall:start -->\n## Everyone\n\n[![421 people built Codewhale](assets/contributors.png)](#everyone)\n\n421 people have code, co-authored commits, carried-forward pull requests or credited reports in Codewhale. GitHub's contributor list shows only commit authors whose email is linked to their account, so it counts 203 of them. If your commits are missing there, add your commit email to your GitHub account and they will link.\n\n[0gl20shk0sbt36](https://github.com/0gl20shk0sbt36) · [1Git2Clone](https://github.com/1Git2Clone) · [20bytes](https://github.com/20bytes) · [47Cid](https://github.com/47Cid) · [7jrxt42BxFZo4iAnN4CX](https://github.com/7jrxt42BxFZo4iAnN4CX) · [8sero](https://github.com/8sero) · [A-Corner](https://github.com/A-Corner) · [aboimpinto](https://github.com/aboimpinto) · [adity982](https://github.com/adity982) · [AdityaVG13](https://github.com/AdityaVG13) · [AGSaturn](https://github.com/AGSaturn) · [ai-dengwsh](https://github.com/ai-dengwsh) · [aiapienthusiast](https://github.com/aiapienthusiast) · [Aitensa](https://github.com/Aitensa) · [AiurArtanis](https://github.com/AiurArtanis) · [alitvak69](https://github.com/alitvak69) · [all-lopezg](https://github.com/all-lopezg) · [alvin1](https://github.com/alvin1) · [amuthantamil](https://github.com/amuthantamil) · [AnaheimEX](https://github.com/AnaheimEX) · [Andrea-Bruno](https://github.com/Andrea-Bruno) · [Angel-Hair](https://github.com/Angel-Hair) · [angus-guo](https://github.com/angus-guo) · [angziii](https://github.com/angziii) · [AnonymousUser443](https://github.com/AnonymousUser443) · [antarikshraya](https://github.com/antarikshraya) · [Anyexyz](https://github.com/Anyexyz) · [Apeiron0w0](https://github.com/Apeiron0w0) · [aqilaziz](https://github.com/aqilaziz) · [AresNing](https://github.com/AresNing) · [ArronAI007](https://github.com/ArronAI007) · [Artenx](https://github.com/Artenx) · [asdfg314284230](https://github.com/asdfg314284230) · [Asteria1028](https://github.com/Asteria1028) · [asto18089](https://github.com/asto18089) · [atmosuwiryo](https://github.com/atmosuwiryo) · [axobase001](https://github.com/axobase001) · [aznikline](https://github.com/aznikline) · [baendlorel](https://github.com/baendlorel) · [banqii](https://github.com/banqii) · [Beltran12138](https://github.com/Beltran12138) · [bevis-wong](https://github.com/bevis-wong) · [BH8GCJ](https://github.com/BH8GCJ) · [BigBenLabs](https://github.com/BigBenLabs) · [Biilow](https://github.com/Biilow) · [bistack](https://github.com/bistack) · [BrathonBai](https://github.com/BrathonBai) · [bruce6135](https://github.com/bruce6135) · [BryonGo](https://github.com/BryonGo) · [buiducnhat](https://github.com/buiducnhat) · [buko](https://github.com/buko) · [BX166](https://github.com/BX166) · [c020627](https://github.com/c020627) · [cacdcaecawae](https://github.com/cacdcaecawae) · [caeserchen](https://github.com/caeserchen) · [caiyilian](https://github.com/caiyilian) · [CCChisato](https://github.com/CCChisato) · [ccomma](https://github.com/ccomma) · [cenab](https://github.com/cenab) · [ChaceLyee2101](https://github.com/ChaceLyee2101) · [chatman-media](https://github.com/chatman-media) · [chennest](https://github.com/chennest) · [chinaqy110](https://github.com/chinaqy110) · [chnjames](https://github.com/chnjames) · [chuntseevolving](https://github.com/chuntseevolving) · [ci4ic4](https://github.com/ci4ic4) · [cian-lien](https://github.com/cian-lien) · [cmdcorp6534](https://github.com/cmdcorp6534) · [cmyyy](https://github.com/cmyyy) · [codepgq](https://github.com/codepgq) · [Colorful-glassblock](https://github.com/Colorful-glassblock) · [ComeFromTheMars](https://github.com/ComeFromTheMars) · [CrepuscularIRIS](https://github.com/CrepuscularIRIS) · [ct-jaryn](https://github.com/ct-jaryn) · [ctxyao](https://github.com/ctxyao) · [cy2311](https://github.com/cy2311) · [cyq1017](https://github.com/cyq1017) · [czf0718](https://github.com/czf0718) · [dajiaohuang](https://github.com/dajiaohuang) · [DarrellThomas](https://github.com/DarrellThomas) · [demian-welt](https://github.com/demian-welt) · [Dev-next-gen](https://github.com/Dev-next-gen) · [dfwqdyl-ui](https://github.com/dfwqdyl-ui) · [DI-HUO-MING-YI](https://github.com/DI-HUO-MING-YI) · [DieMoe233](https://github.com/DieMoe233) · [DingYong4223Line truncated
"sha256": "a5e5d0d97f728e97ff02e6308d10a2f505a924bcafd2866be42e6aad834bc164",
"content": "# Contributors\n\nCodewhale is built in the open with a growing community of contributors. Every\nissue report and pull request is real project work — welcome at any experience\nlevel. This is the full per-PR contributor record, grouped into release/time\nbands with the newest band first so it stays scannable. Expand any band to see\neveryone.\n\nFor the live list, see the\n[GitHub contributors page](https://github.com/codewhale-hq/CodeWhale/graphs/contributors),\n[`AUTHOR_MAP`](https://github.com/codewhale-hq/CodeWhale/blob/main/.github/AUTHOR_MAP),\nand [CHANGELOG.md](../CHANGELOG.md).\n\n<!-- contributor-wall:start -->\n## Everyone\n\n[![421 people built Codewhale](assets/contributors.png)](#everyone)\n\n421 people have code, co-authored commits, carried-forward pull requests or credited reports in Codewhale. GitHub's contributor list shows only commit authors whose email is linked to their account, so it counts 203 of them. If your commits are missing there, add your commit email to your GitHub account and they will link.\n\n[0gl20shk0sbt36](https://github.com/0gl20shk0sbt36) · [1Git2Clone](https://github.com/1Git2Clone) · [20bytes](https://github.com/20bytes) · [47Cid](https://github.com/47Cid) · [7jrxt42BxFZo4iAnN4CX](https://github.com/7jrxt42BxFZo4iAnN4CX) · [8sero](https://github.com/8sero) · [A-Corner](https://github.com/A-Corner) · [aboimpinto](https://github.com/aboimpinto) · [adity982](https://github.com/adity982) · [AdityaVG13](https://github.com/AdityaVG13) · [AGSaturn](https://github.com/AGSaturn) · [ai-dengwsh](https://github.com/ai-dengwsh) · [aiapienthusiast](https://github.com/aiapienthusiast) · [Aitensa](https://github.com/Aitensa) · [AiurArtanis](https://github.com/AiurArtanis) · [alitvak69](https://github.com/alitvak69) · [all-lopezg](https://github.com/all-lopezg) · [alvin1](https://github.com/alvin1) · [amuthantamil](https://github.com/amuthantamil) · [AnaheimEX](https://github.com/AnaheimEX) · [Andrea-Bruno](https://github.com/Andrea-Bruno) · [Angel-Hair](https://github.com/Angel-Hair) · [angus-guo](https://github.com/angus-guo) · [angziii](https://github.com/angziii) · [AnonymousUser443](https://github.com/AnonymousUser443) · [antarikshraya](https://github.com/antarikshraya) · [Anyexyz](https://github.com/Anyexyz) · [Apeiron0w0](https://github.com/Apeiron0w0) · [aqilaziz](https://github.com/aqilaziz) · [AresNing](https://github.com/AresNing) · [ArronAI007](https://github.com/ArronAI007) · [Artenx](https://github.com/Artenx) · [asdfg314284230](https://github.com/asdfg314284230) · [Asteria1028](https://github.com/Asteria1028) · [asto18089](https://github.com/asto18089) · [atmosuwiryo](https://github.com/atmosuwiryo) · [axobase001](https://github.com/axobase001) · [aznikline](https://github.com/aznikline) · [baendlorel](https://github.com/baendlorel) · [banqii](https://github.com/banqii) · [Beltran12138](https://github.com/Beltran12138) · [bevis-wong](https://github.com/bevis-wong) · [BH8GCJ](https://github.com/BH8GCJ) · [BigBenLabs](https://github.com/BigBenLabs) · [Biilow](https://github.com/Biilow) · [bistack](https://github.com/bistack) · [BrathonBai](https://github.com/BrathonBai) · [bruce6135](https://github.com/bruce6135) · [BryonGo](https://github.com/BryonGo) · [buiducnhat](https://github.com/buiducnhat) · [buko](https://github.com/buko) · [BX166](https://github.com/BX166) · [c020627](https://github.com/c020627) · [cacdcaecawae](https://github.com/cacdcaecawae) · [caeserchen](https://github.com/caeserchen) · [caiyilian](https://github.com/caiyilian) · [CCChisato](https://github.com/CCChisato) · [ccomma](https://github.com/ccomma) · [cenab](https://github.com/cenab) · [ChaceLyee2101](https://github.com/ChaceLyee2101) · [chatman-media](https://github.com/chatman-media) · [chennest](https://github.com/chennest) · [chinaqy110](https://github.com/chinaqy110) · [chnjames](https://github.com/chnjames) · [chuntseevolving](https://github.com/chuntseevolving) · [ci4ic4](https://github.com/ci4ic4) · [cian-lien](https://github.com/cian-lien) · [cmdcorp6534](https://github.com/cmdcorp6534) · [cmyyy](https://github.com/cmyyy) · [codepgq](https://github.com/codepgq) · [Colorful-glassblock](https://github.com/Colorful-glassblock) · [ComeFromTheMars](https://github.com/ComeFromTheMars) · [CrepuscularIRIS](https://github.com/CrepuscularIRIS) · [ct-jaryn](https://github.com/ct-jaryn) · [ctxyao](https://github.com/ctxyao) · [cy2311](https://github.com/cy2311) · [cyq1017](https://github.com/cyq1017) · [czf0718](https://github.com/czf0718) · [dajiaohuang](https://github.com/dajiaohuang) · [DarrellThomas](https://github.com/DarrellThomas) · [demian-welt](https://github.com/demian-welt) · [Dev-next-gen](https://github.com/Dev-next-gen) · [dfwqdyl-ui](https://github.com/dfwqdyl-ui) · [DI-HUO-MING-YI](https://github.com/DI-HUO-MING-YI) · [DieMoe233](https://github.com/DieMoe233) · [DingYong4223Line truncated
},
{
"path": "docs/RELEASE_CHECKLIST.md",
@@ -357,8 +357,8 @@
},
{
"path": "docs/MODES.md",
"sha256": "06bc6d00b279d7a3d0479b370893a900139e593a6a59db6b0d574e621f86363e",
"content": "# Modes and Permission Postures\n\n> 阅读简体中文版:[zh_hans/MODES.md](zh_hans/MODES.md)\n\nCodewhale has three related concepts:\n\n- **TUI mode**: what kind of visible interaction you're in (Plan/Work/Operate).\n- **Permission posture**: how aggressively the UI asks before executing tools.\n- **Workflow**: named steps that coordinate sub-agents, with dependencies and\n results, available in any TUI mode.\n\nModel selection is separate. `--model auto` and `/model auto` route each turn to\na concrete model and thinking level; they are not TUI modes and are not part of\nthe `Tab` cycle.\n\nWorkflow runs named steps through the same sub-agent runtime; Fleet manages\ntheir saved roles and model assignments. A workflow adds ordering, result\nhandoffs, gates and a progress view. The active mode and permission posture\nstill control what each step may execute.\n\nBefore assigning steps, query `agent(action=\"roster\")` for the saved Fleet\nmodels and roles. A plan child can select a listed model with `model`, or use\nthe saved `role`/`profile` assignment. An Exact Fleet fixes each member's route.\n\n## TUI Modes\n\nPress `Tab` to complete composer menus or cycle through the visible modes\nwhen the composer is empty: **Plan → Work → Operate → Plan**. `Tab` never sends\nor queues composer text; use `Enter` to send or queue it.\nPress `Shift+Tab` to cycle permission posture (Ask → Auto-Review → Full Access).\nEvery change of effective permission writes a `Permissions changed to …` line\nin the transcript.\nPress `Ctrl+T` to cycle reasoning effort.\nRun `/mode` to open the mode picker, or switch directly with `/mode work`,\n`/mode plan`, or `/mode operate`.\n\n- **Plan**: design-first prompting. The stable primitive names remain familiar, but the runtime centrally refuses file mutation and shell execution. Read-only inspection and policy-allowed research, including deferred Web search/fetch, remain available. When a Plan turn successfully finishes a nonempty response, including a prose-only plan, the TUI asks how to continue: Work (Ask), Work (Auto-Review), or keep planning. Work sends the exact approved response as a normal follow-up and preserves that turn's existing To-dos, or adds one pending plan item without replacing unrelated work.\n- **Work** (internally `agent`): ordinary multi-step execution. The first-turn toolbox includes `read`, `write`, `edit`, `file_search`, `bash`, `agent`, `workflow`, `todo_write`, and `load_skill`, plus `create_goal`, `get_goal`, `update_goal`, and `task_shell_wait` so goal and background-shell controls are available without discovery. The agent calls `create_goal` when a request is a durable objective; the host never infers a goal from wording, and the contract must not tell the model otherwise. Approval, sandbox, repository law, and managed policy decide what may execute.\n- **Operate**: Work at full strength, the \"ultra\" mode. Same tools and authority as Work; the difference is how they are used. Each substantive request becomes a goal (`create_goal`), the plan stays visible (`todo_write`), multi-part work runs through `workflow` and parallel `agent` workers, non-trivial changes get an independent reviewer before they are called done, long commands keep running in the background, and recurring or watch-type work is offered as an `automation` (created after approval). It keeps going until the goal is verified, blocked on you, or paused.\n\n`Act` and `/mode act` remain compatibility aliases for Work. Saved settings\nstill normalize to the internal value `agent`.\n\n### Tool availability by mode\n\n| Tool family | Plan | Work | Operate |\n|:---|:---:|:---:|:---:|\n| `read` and policy-allowed deferred research tools | yes | yes | yes |\n| `write` and `edit` | visible names; execution denied | approval- and policy-gated | same as Work |\n| `bash` | visible name; execution denied | approval- and policy-gated | same as Work; delegation is preferred when parallelism or isolation helps |\n| `agent` | yes, subject to child-depth authority | yes, subject to child-depth authority | yes, subject to child-depth authority |\n| Deferred native, MCP, and plugin tools | discoverable through `tool_search` when policy permits | same | same |\n| Paid or external-service tools | follows permission posture | follows permission posture | follows permission posture |\n| Access outside the workspace root | explicit trusted paths only | only through trusted paths or trust mode | same trusted-path/trust policy as Work; fleet profiles never widen it |\n\nOperate changes scheduling emphasis, not authority. It neither adds a\nmode-specific tool denial nor bypasses the active approval, sandbox, shell,\nask-rule, repository-law, or managed-policy boundary. Plan remains the\nmode-specific execution boundary for shell and write-capable tools; that\nauthority difference does not require a different primitive vocabulary.\n\n### Operate loop (one screen)\n\n```text\nUser Line truncated
"sha256": "e327b2042c9b782660cb0d6de9f4208364537ef1052a456a45332a61b1ee2468",
"content": "# Modes and Permission Postures\n\n> 阅读简体中文版:[zh_hans/MODES.md](zh_hans/MODES.md)\n\nCodewhale has three related concepts:\n\n- **TUI mode**: what kind of visible interaction you're in (Plan/Work/Operate).\n- **Permission posture**: how aggressively the UI asks before executing tools.\n- **Workflow**: named steps that coordinate sub-agents, with dependencies and\n results, available in any TUI mode.\n\nModel selection is separate. `--model auto` and `/model auto` route each turn to\na concrete model and thinking level; they are not TUI modes and are not part of\nthe `Tab` cycle.\n\nWorkflow runs named steps through the same sub-agent runtime; Fleet manages\ntheir saved roles and model assignments. A workflow adds ordering, result\nhandoffs, gates and a progress view. The active mode and permission posture\nstill control what each step may execute.\n\nBefore assigning steps, query `agent(action=\"roster\")` for the saved Fleet\nmodels and roles. A plan child can select a listed model with `model`, or use\nthe saved `role`/`profile` assignment. An Exact Fleet fixes each member's route.\n\n## TUI Modes\n\nPress `Tab` to complete composer menus or cycle through the visible modes\nwhen the composer is empty: **Plan → Work → Operate → Plan**. `Tab` never sends\nor queues composer text; use `Enter` to send or queue it.\nPress `Shift+Tab` to cycle permission posture (Ask → Auto-Review → Full Access).\nEvery change of effective permission writes a `Permissions changed to …` line\nin the transcript.\nPress `Ctrl+T` to cycle reasoning effort.\nRun `/mode` to open the mode picker, or switch directly with `/mode work`,\n`/mode plan`, or `/mode operate`.\n\n- **Plan**: design-first prompting. The stable primitive names remain familiar, but the runtime centrally refuses file mutation and shell execution. Read-only inspection and policy-allowed research, including deferred Web search/fetch, remain available. When a Plan turn successfully finishes a nonempty response, including a prose-only plan, the TUI asks how to continue: Work (Ask), Work (Auto-Review), or keep planning. Work sends the exact approved response as a normal follow-up and preserves that turn's existing To-dos, or adds one pending plan item without replacing unrelated work.\n- **Work** (internally `agent`): ordinary multi-step execution. The first-turn toolbox includes `read`, `write`, `edit`, `file_search`, `bash`, `agent`, `workflow`, `todo_write`, and `load_skill`, plus `create_goal`, `get_goal`, `update_goal`, and `task_shell_wait` so goal and background-shell controls are available without discovery. The agent calls `create_goal` when a request is a durable objective; the host never infers a goal from wording, and the contract must not tell the model otherwise. Approval, sandbox, repository law, and managed policy decide what may execute.\n- **Operate**: Work at full strength, the \"ultra\" mode. Same tools and authority as Work; the difference is how they are used. Each substantive request becomes a goal (`create_goal`), the plan stays visible (`todo_write`), multi-part work runs through `workflow` and parallel `agent` workers, non-trivial changes get an independent reviewer before they are called done, long commands keep running in the background, and recurring or watch-type work is offered as an `automation` (created after approval). It keeps going until the goal is verified, blocked on you, or paused.\n\n`Act` and `/mode act` remain compatibility aliases for Work. Saved settings\nstill normalize to the internal value `agent`.\n\n### Tool availability by mode\n\n| Tool family | Plan | Work | Operate |\n|:---|:---:|:---:|:---:|\n| `read` and policy-allowed deferred research tools | yes | yes | yes |\n| `write` and `edit` | visible names; execution denied | approval- and policy-gated | same as Work |\n| `bash` | visible name; execution denied | approval- and policy-gated | same as Work; delegation is preferred when parallelism or isolation helps |\n| `agent` | yes, subject to child-depth authority | yes, subject to child-depth authority | yes, subject to child-depth authority |\n| Deferred native, MCP, and plugin tools | discoverable through `tool_search` when policy permits | same | same |\n| Paid or external-service tools | follows permission posture | follows permission posture | follows permission posture |\n| Access outside the workspace root | explicit trusted paths only | only through trusted paths or trust mode | same trusted-path/trust policy as Work; fleet profiles never widen it |\n\nOperate changes scheduling emphasis, not authority. It neither adds a\nmode-specific tool denial nor bypasses the active approval, sandbox, shell,\nask-rule, repository-law, or managed-policy boundary. Plan remains the\nmode-specific execution boundary for shell and write-capable tools; that\nauthority difference does not require a different primitive vocabulary.\n\n### Operate loop (one screen)\n\n```text\nUser Line truncated
},
{
"path": "docs/public-surface-facts.json",
@@ -373,8 +373,8 @@
"sources": [
{
"path": "docs/ARCHITECTURE.md",
"sha256": "a24f55a1a6c0f8c960ef6cee23ac63710d3c92f0397868e8a55f57b6289629df",
"content": "# Codewhale Architecture\n\n> 阅读简体中文版:[zh_hans/ARCHITECTURE.md](zh_hans/ARCHITECTURE.md)。\n\nCodewhale Engine is the existing Rust execution runtime. The\n[Runtime API](RUNTIME_API.md) is its client interface, and\n[TypeScript mods](EXTENSIONS.md) contribute reviewed extensions.\n\nCurrent boundary note (read the workspace version from `Cargo.toml`; this\nboundary has held since v0.9.1):\n- `crates/tui` is still the live end-user runtime for the TUI, runtime API, task manager, and tool execution loop.\n- Other workspace crates are being split out incrementally, but they are not yet the sole runtime source of truth.\n- `crates/net` owns network policy/auditing, destination and DNS guards, proxy resolution, and Claude import planning. Host tool context, transport grants, redirects and response limits stay with their callers. Pure extension-host settings belong to `crates/config`.\n- `crates/notify` owns shared notification payload redaction, attention/category gates, sound policy, and local audio dispatch. TUI retains terminal sinks and native hosts retain banner submission.\n\n<img src=\"assets/architecture.webp\" alt=\"Codewhale architecture: clients reach the one turn loop, Engine::run_turn, through the CLI or the Runtime API; the engine uses crates/core to build requests, runs tools under approval and sandbox policy, routes to any provider through crates/config, and logs everything model-visible to the session store.\" width=\"1000\">\n\nThe diagram draws the Runtime API and the task manager as their own boxes; today\nboth are hosted inside `crates/tui`, as the note above says. The GPUI desktop\napp and the VS Code extension live outside this repository. The arrow from the\nengine to the session log follows the \"model-visible means logged\" contract\nrather than a traced write path.\n- `crates/tui` is being split into crates sized as agent work partitions:\n contracts, utilities, services, providers, runtime, leaf feature crates,\n TUI views and hosts, in the order `docs/design/TUI_DECONSTRUCTION.md`\n records. The TUI stays the only crate that writes to the terminal. Until a\n module has moved, its path under `crates/tui/src` is still where it lives.\n- The LSP subsystem (`crates/tui/src/lsp/`) is fully wired into the engine's\n post-tool-execution path (`core/engine/lsp_hooks.rs`), providing inline\n diagnostics after `File` write, edit, and patch actions.\n- The swarm agent system was removed in v0.8.5. The active sub-agent surface is\n the single `agent` tool; persistent RLM sessions are available through the\n deferred `rlm` action family.\n No model-visible swarm tool remains in the active codebase.\n\n## High-Level Overview\n\n```\n┌─────────────────────────────────────────────────────────────────┐\n│ User Interface │\n│ ┌─────────────────┐ ┌─────────────────┐ ┌────────────────┐ │\n│ │ TUI (ratatui) │ │ One-shot Mode │ │ Config/CLI │ │\n│ └────────┬────────┘ └────────┬────────┘ └────────┬───────┘ │\n└───────────┼─────────────────────┼────────────────────┼──────────┘\n │ │ │\n ▼ ▼ ▼\n┌─────────────────────────────────────────────────────────────────┐\n│ Core Engine │\n│ ┌─────────────────────────────────────────────────────────┐ │\n│ │ Agent Loop (core/engine.rs) │ │\n│ │ ┌─────────┐ ┌─────────────┐ ┌──────────────────────┐ │ │\n│ │ │ Session │ │ Turn Mgmt │ │ Tool Orchestration │ │ │\n│ │ └─────────┘ └─────────────┘ └──────────────────────┘ │ │\n│ └─────────────────────────────────────────────────────────┘ │\n└─────────Line truncated
"sha256": "ba6a71173f089fbc0e1ff467ca1f0f67e20a0df0a9f1961596d89102acec926d",
"content": "# Codewhale Architecture\n\n> 阅读简体中文版:[zh_hans/ARCHITECTURE.md](zh_hans/ARCHITECTURE.md)。\n\nCodewhale Engine is the existing Rust execution runtime. The\n[Runtime API](RUNTIME_API.md) is its client interface, and\n[TypeScript mods](EXTENSIONS.md) contribute reviewed extensions.\n\nCurrent boundary note (read the workspace version from `Cargo.toml`; this\nboundary has held since v0.9.1):\n- `crates/tui` is still the live end-user runtime for the TUI, runtime API, task manager, and tool execution loop.\n- Other workspace crates are being split out incrementally, but they are not yet the sole runtime source of truth.\n- `crates/net` owns network policy/auditing, destination and DNS guards, proxy resolution, and Claude import planning. Host tool context, transport grants, redirects and response limits stay with their callers. Pure extension-host settings belong to `crates/config`.\n- `crates/notify` owns shared notification payload redaction, attention/category gates, sound policy, and local audio dispatch. TUI retains terminal sinks and native hosts retain banner submission.\n- `crates/util` owns shared string/path/atomic-write helpers, confined workspace file access, link and file-identity checks, bundle file readers, the `.git` name rule and the exclusive file lock.\n- `crates/dependencies` owns external-tool resolution and bounded subprocess probes (git, python, node, bun); `crates/snapshot` the pre/post-turn side-repo snapshots; `crates/lsp` the language-server client; `crates/repl` the Python REPL runtime and RLM context files.\n- `crates/skill-install` owns the community-skill installer (GitHub and tarball sources, the curated registry, bounded extraction, install and trust markers), the package content digest and the shared `SKILL.md` frontmatter reader. Its SSRF DNS pin and shortened timeouts are relaxed only under its dev-dependency-only `test-support` feature.\n\n<img src=\"assets/architecture.webp\" alt=\"Codewhale architecture: clients reach the one turn loop, Engine::run_turn, through the CLI or the Runtime API; the engine uses crates/core to build requests, runs tools under approval and sandbox policy, routes to any provider through crates/config, and logs everything model-visible to the session store.\" width=\"1000\">\n\nThe diagram draws the Runtime API and the task manager as their own boxes; today\nboth are hosted inside `crates/tui`, as the note above says. The GPUI desktop\napp and the VS Code extension live outside this repository. The arrow from the\nengine to the session log follows the \"model-visible means logged\" contract\nrather than a traced write path.\n- `crates/tui` is being split into crates sized as agent work partitions:\n contracts, utilities, services, providers, runtime, leaf feature crates,\n TUI views and hosts, in the order `docs/design/TUI_DECONSTRUCTION.md`\n records. The TUI stays the only crate that writes to the terminal. Until a\n module has moved, its path under `crates/tui/src` is still where it lives.\n- The LSP subsystem (`crates/lsp/src/`) is fully wired into the engine's\n post-tool-execution path (`core/engine/lsp_hooks.rs`), providing inline\n diagnostics after `File` write, edit, and patch actions.\n- The swarm agent system was removed in v0.8.5. The active sub-agent surface is\n the single `agent` tool; persistent RLM sessions are available through the\n deferred `rlm` action family.\n No model-visible swarm tool remains in the active codebase.\n\n## High-Level Overview\n\n```\n┌─────────────────────────────────────────────────────────────────┐\n│ User Interface │\n│ ┌─────────────────┐ ┌─────────────────┐ ┌────────────────┐ │\n│ │ TUI (ratatui) │ │ One-shot Mode │ │ Config/CLI │ │\n│ └────────┬────────┘ └────────┬────────┘ └────────┬───────┘ │\n└───────────┼─────────────────────┼────────────────────┼──────────┘\n │ │ │\n ▼ ▼ ▼\n┌─────────────────────────────────────────────────────────────────┐\n│ Core Engine │\n│ ┌─────────────────────────────────────Line truncated
}
]
},
@@ -394,8 +394,8 @@
},
{
"path": "docs/LSP_PHP_CUSTOM.md",
"sha256": "2dc48d7688a92844d6a5adf682e457b9bcb30f54c64c838af254d35feb4b659f",
"content": "# LSP: PHP Support & Custom Language Server Extension\n\n> 阅读简体中文版:[zh_hans/LSP_PHP_CUSTOM.md](zh_hans/LSP_PHP_CUSTOM.md)\n>\n> v0.8.65+ | `codex/lsp-php-custom-servers`\n\n## Overview\n\nThis feature adds **PHP** to the built-in LSP language registry and introduces a\n`[lsp.custom]` config section so users can register arbitrary LSP servers by\nfile extension — covering languages absent from the built-in `Language` enum\n(Ruby, C#, Swift, Lua, etc.).\n\n## Changes\n\n### 1. PHP built-in support\n\n- `Language::Php` variant added to the enum in `crates/tui/src/lsp/registry.rs`\n- `.php` files are detected and routed to `intelephense --stdio` by default\n- User can override via `[lsp.servers].php`\n\n### 2. Custom LSP server extension\n\nNew struct `CustomLspDef` (defined in `crates/tui/src/lsp/mod.rs` and mirrored\nin `crates/config/src/lib.rs`):\n\n```rust\npub struct CustomLspDef {\n pub language_id: String, // LSP languageId for textDocument/didOpen\n pub command: String, // executable to spawn\n pub args: Vec<String>, // arguments (default empty)\n}\n```\n\nNew config field `LspConfig.custom: HashMap<String, CustomLspDef>` — keyed by\nfile extension (without dot), e.g. `\"rb\"`, `\"cs\"`, `\"swift\"`.\n\nIn `LspManager::diagnostics_for`, when the built-in registry returns\n`Language::Other`, the manager checks the user's custom table before giving up.\nCustom servers get their own lazy-spawn transport map and once-per-extension\nmissing-binary warnings (no log spam).\n\n### 3. Transport generalization\n\n`StdioLspTransport::spawn` now accepts `&str language_id` instead of\n`Language`, so both built-in and custom servers share the same transport\nimplementation. The old `Language` import is removed from `client.rs`.\n\n### 4. Polling pipeline extracted\n\n`poll_diagnostics` is a new private method that both built-in and custom\ndiagnostics paths share — eliminating duplicated call/wait/filter/sort/truncate\nlogic.\n\n## Configuration\n\n### Built-in PHP (enabled by default if `intelephense` is on PATH)\n\n```toml\n# No config needed — PHP .php files are detected automatically.\n# Override the server if desired:\n[lsp.servers]\nphp = [\"phpactor\", \"language-server\"]\n```\n\n### Custom language servers\n\n```toml\n[lsp.custom.rb]\ncommand = \"ruby-lsp\"\nargs = [\"--stdio\"]\nlanguage_id = \"ruby\"\n\n[lsp.custom.cs]\ncommand = \"csharp-ls\"\nlanguage_id = \"csharp\"\n\n[lsp.custom.swift]\ncommand = \"sourcekit-lsp\"\nlanguage_id = \"swift\"\n```\n\nKeys are file extensions (no leading dot). The `args` field defaults to empty.\n`language_id` must match what the LSP server expects in `textDocument/didOpen`.\n\n## Architecture\n\n```\nedit_file / write_file / apply_patch success\n │\n ▼\n LspManager.diagnostics_for(file)\n │\n ├── custom_for_extension(file) ── found? ──► transport_for_custom(ext, def)\n │ │\n ├── detect_language(file) ── Other? ──► return None (skip)\n │\n └── transport_for(lang)\n │\n ▼\n poll_diagnostics(file, text, transport)\n │\n ▼\n DiagnosticBlock → injected into session message stream\n```\n\n## Verification\n\n```\ncargo test -p codewhale-tui --lib lsp::\n# 32 tests passed (3 new: detects_php_extension, language_ids_for_php,\n# server_for_php_is_intelephense)\ncargo clippy -p codewhale-tui --lib\n# lsp module: zero new warnings\n```\n\n## Files touched\n\n| File | Change |\n|------|--------|\n| `crates/tui/src/lsp/registry.rs` | +Php variant, detection, server mapping, tests |\n| `crates/tui/src/lsp/mod.rs` | +CustomLspDef, LspConfig.custom, LspManager custom fallback |\n| `crates/tui/src/lsp/client.rs` | spawn accepts &str language_id |\n| `crates/tui/src/config.rs` | LspConfigToml.custom + into_runtime |\n| `crates/config/src/lib.rs` | LspConfigToml.custom + CustomLspDef |\n| `config.example.toml` | PHP docs + custom extension examples |\n"
"sha256": "be77d5f68a990d452cc99523b4d9982267c18a7d1b30766d31e5385e0bfed453",
"content": "# LSP: PHP Support & Custom Language Server Extension\n\n> 阅读简体中文版:[zh_hans/LSP_PHP_CUSTOM.md](zh_hans/LSP_PHP_CUSTOM.md)\n>\n> v0.8.65+ | `codex/lsp-php-custom-servers`\n\n## Overview\n\nThis feature adds **PHP** to the built-in LSP language registry and introduces a\n`[lsp.custom]` config section so users can register arbitrary LSP servers by\nfile extension — covering languages absent from the built-in `Language` enum\n(Ruby, C#, Swift, Lua, etc.).\n\n## Changes\n\n### 1. PHP built-in support\n\n- `Language::Php` variant added to the enum in `crates/lsp/src/registry.rs`\n- `.php` files are detected and routed to `intelephense --stdio` by default\n- User can override via `[lsp.servers].php`\n\n### 2. Custom LSP server extension\n\nNew struct `CustomLspDef` (defined in `crates/lsp/src/lib.rs` and mirrored\nin `crates/config/src/lib.rs`):\n\n```rust\npub struct CustomLspDef {\n pub language_id: String, // LSP languageId for textDocument/didOpen\n pub command: String, // executable to spawn\n pub args: Vec<String>, // arguments (default empty)\n}\n```\n\nNew config field `LspConfig.custom: HashMap<String, CustomLspDef>` — keyed by\nfile extension (without dot), e.g. `\"rb\"`, `\"cs\"`, `\"swift\"`.\n\nIn `LspManager::diagnostics_for`, when the built-in registry returns\n`Language::Other`, the manager checks the user's custom table before giving up.\nCustom servers get their own lazy-spawn transport map and once-per-extension\nmissing-binary warnings (no log spam).\n\n### 3. Transport generalization\n\n`StdioLspTransport::spawn` now accepts `&str language_id` instead of\n`Language`, so both built-in and custom servers share the same transport\nimplementation. The old `Language` import is removed from `client.rs`.\n\n### 4. Polling pipeline extracted\n\n`poll_diagnostics` is a new private method that both built-in and custom\ndiagnostics paths share — eliminating duplicated call/wait/filter/sort/truncate\nlogic.\n\n## Configuration\n\n### Built-in PHP (enabled by default if `intelephense` is on PATH)\n\n```toml\n# No config needed — PHP .php files are detected automatically.\n# Override the server if desired:\n[lsp.servers]\nphp = [\"phpactor\", \"language-server\"]\n```\n\n### Custom language servers\n\n```toml\n[lsp.custom.rb]\ncommand = \"ruby-lsp\"\nargs = [\"--stdio\"]\nlanguage_id = \"ruby\"\n\n[lsp.custom.cs]\ncommand = \"csharp-ls\"\nlanguage_id = \"csharp\"\n\n[lsp.custom.swift]\ncommand = \"sourcekit-lsp\"\nlanguage_id = \"swift\"\n```\n\nKeys are file extensions (no leading dot). The `args` field defaults to empty.\n`language_id` must match what the LSP server expects in `textDocument/didOpen`.\n\n## Architecture\n\n```\nedit_file / write_file / apply_patch success\n │\n ▼\n LspManager.diagnostics_for(file)\n │\n ├── custom_for_extension(file) ── found? ──► transport_for_custom(ext, def)\n │ │\n ├── detect_language(file) ── Other? ──► return None (skip)\n │\n └── transport_for(lang)\n │\n ▼\n poll_diagnostics(file, text, transport)\n │\n ▼\n DiagnosticBlock → injected into session message stream\n```\n\n## Verification\n\n```\ncargo test -p codewhale-tui --lib lsp::\n# 32 tests passed (3 new: detects_php_extension, language_ids_for_php,\n# server_for_php_is_intelephense)\ncargo clippy -p codewhale-tui --lib\n# lsp module: zero new warnings\n```\n\n## Files touched\n\n| File | Change |\n|------|--------|\n| `crates/lsp/src/registry.rs` | +Php variant, detection, server mapping, tests |\n| `crates/lsp/src/lib.rs` | +CustomLspDef, LspConfig.custom, LspManager custom fallback |\n| `crates/lsp/src/client.rs` | spawn accepts &str language_id |\n| `crates/tui/src/config.rs` | LspConfigToml.custom + into_runtime |\n| `crates/config/src/lib.rs` | LspConfigToml.custom + CustomLspDef |\n| `config.example.toml` | PHP docs + custom extension examples |\n"
}
]
}
+81 -55
View File
@@ -2,13 +2,13 @@
"format": "codewhale.features.v1",
"source": {
"path": "docs/features.toml",
"sha256": "0a0148946fe92a675a396ffd207fbab3debd1bcdec70c8e3227baf9340b30ffa"
"sha256": "c1673c4a1d668f3f9fe82c09ea016b9e3bf1f71a4f0c1076253af5db06c80a79"
},
"features": [
{
"id": "unix-uninstall",
"name": "Receipt-owned Unix uninstall",
"summary": "Preview or remove this receipt-owned Unix release installation while preserving user data and other installations.",
"name": "Receipt-owned uninstall",
"summary": "Remove a receipt-owned release install or print its package manager's command; data kept unless --purge-data.",
"status": "experimental",
"surfaces": {
"tui": "preview"
@@ -454,6 +454,32 @@
"docs": "docs/CONFIGURATION.md#goal-loop-goal",
"registryLine": 371
},
{
"id": "thread-scheduled-turns",
"name": "Reminders into the same thread",
"summary": "send_later, automations and /v1/triggers can deliver a scheduled message as a new turn in an existing thread.",
"status": "experimental",
"surfaces": {
"api": "preview"
},
"since": "unreleased",
"flag": null,
"docs": "docs/RUNTIME_API.md",
"registryLine": 380
},
{
"id": "task-suggestions",
"name": "Suggested tasks",
"summary": "The tasks tool's suggest action offers the person a task as a Start/Hide card; it starts nothing and needs no approval.",
"status": "experimental",
"surfaces": {
"desktop": "preview"
},
"since": "unreleased",
"flag": null,
"docs": null,
"registryLine": 390
},
{
"id": "todos",
"name": "Todo list",
@@ -466,7 +492,7 @@
"since": null,
"flag": null,
"docs": "docs/TOOL_SURFACE.md",
"registryLine": 380
"registryLine": 399
},
{
"id": "composer-queue-steer",
@@ -480,7 +506,7 @@
"since": null,
"flag": null,
"docs": "docs/KEYBINDINGS.md",
"registryLine": 389
"registryLine": 408
},
{
"id": "receipts",
@@ -494,7 +520,7 @@
"since": "0.10.1",
"flag": null,
"docs": "docs/RECEIPTS.md",
"registryLine": 398
"registryLine": 417
},
{
"id": "preview-request",
@@ -507,7 +533,7 @@
"since": null,
"flag": null,
"docs": "docs/PREVIEW_REQUEST.md",
"registryLine": 408
"registryLine": 427
},
{
"id": "read-media",
@@ -520,7 +546,7 @@
"since": null,
"flag": null,
"docs": "docs/READ_MEDIA.md",
"registryLine": 417
"registryLine": 436
},
{
"id": "memory",
@@ -534,7 +560,7 @@
"since": null,
"flag": null,
"docs": "docs/MEMORY.md",
"registryLine": 426
"registryLine": 445
},
{
"id": "skills",
@@ -548,7 +574,7 @@
"since": null,
"flag": null,
"docs": "docs/SKILLS.md",
"registryLine": 437
"registryLine": 456
},
{
"id": "plugins",
@@ -562,7 +588,7 @@
"since": null,
"flag": null,
"docs": "docs/PLUGINS.md",
"registryLine": 446
"registryLine": 465
},
{
"id": "computer-use",
@@ -576,7 +602,7 @@
"since": null,
"flag": null,
"docs": "docs/PLUGIN_BUNDLES.md",
"registryLine": 455
"registryLine": 474
},
{
"id": "script-tools",
@@ -589,7 +615,7 @@
"since": null,
"flag": null,
"docs": "docs/CONFIGURATION.md#script-tools-and-overrides",
"registryLine": 464
"registryLine": 483
},
{
"id": "claude-plugin-compat",
@@ -602,7 +628,7 @@
"since": null,
"flag": null,
"docs": "docs/CLAUDE_PLUGIN_COMPAT.md",
"registryLine": 473
"registryLine": 492
},
{
"id": "claude-settings-import",
@@ -615,7 +641,7 @@
"since": null,
"flag": null,
"docs": null,
"registryLine": 482
"registryLine": 501
},
{
"id": "hooks",
@@ -629,7 +655,7 @@
"since": null,
"flag": null,
"docs": "docs/HOOKS.md",
"registryLine": 490
"registryLine": 509
},
{
"id": "lsp",
@@ -643,7 +669,7 @@
"since": null,
"flag": null,
"docs": "docs/LSP_PHP_CUSTOM.md",
"registryLine": 499
"registryLine": 518
},
{
"id": "runtime-api",
@@ -656,7 +682,7 @@
"since": null,
"flag": null,
"docs": "docs/RUNTIME_API.md",
"registryLine": 508
"registryLine": 527
},
{
"id": "compatibility-tool-free-turns",
@@ -669,7 +695,7 @@
"since": "0.10.2",
"flag": null,
"docs": "docs/RUNTIME_API.md#compatibility-execution-limits",
"registryLine": 517
"registryLine": 536
},
{
"id": "runtime-provider-keys",
@@ -682,7 +708,7 @@
"since": "0.10.2",
"flag": null,
"docs": "docs/RUNTIME_API.md#put-v1providersidkey--write-only-credential",
"registryLine": 527
"registryLine": 546
},
{
"id": "headless-exec",
@@ -695,7 +721,7 @@
"since": null,
"flag": null,
"docs": "docs/RUNTIME_API.md",
"registryLine": 537
"registryLine": 556
},
{
"id": "github-review",
@@ -708,7 +734,7 @@
"since": null,
"flag": null,
"docs": "docs/GITHUB_APP.md",
"registryLine": 546
"registryLine": 565
},
{
"id": "web-client",
@@ -721,7 +747,7 @@
"since": null,
"flag": null,
"docs": "docs/WEB.md",
"registryLine": 557
"registryLine": 576
},
{
"id": "keybindings",
@@ -734,7 +760,7 @@
"since": null,
"flag": null,
"docs": "docs/KEYBINDINGS.md",
"registryLine": 566
"registryLine": 585
},
{
"id": "settings",
@@ -748,7 +774,7 @@
"since": null,
"flag": null,
"docs": "docs/CONFIGURATION.md",
"registryLine": 575
"registryLine": 594
},
{
"id": "whats-new-notice",
@@ -761,7 +787,7 @@
"since": "0.10.2",
"flag": null,
"docs": "docs/CONFIGURATION.md#whats-new-after-an-update",
"registryLine": 584
"registryLine": 603
},
{
"id": "themes",
@@ -774,7 +800,7 @@
"since": null,
"flag": null,
"docs": "docs/schemas/user-theme.schema.json",
"registryLine": 594
"registryLine": 613
},
{
"id": "chat-color",
@@ -787,7 +813,7 @@
"since": "0.10.2",
"flag": null,
"docs": "docs/GUIDE.md",
"registryLine": 603
"registryLine": 622
},
{
"id": "localization",
@@ -800,7 +826,7 @@
"since": null,
"flag": null,
"docs": "docs/LOCALIZATION.md",
"registryLine": 613
"registryLine": 632
},
{
"id": "voice",
@@ -814,7 +840,7 @@
"since": null,
"flag": null,
"docs": null,
"registryLine": 622
"registryLine": 641
},
{
"id": "pet",
@@ -828,7 +854,7 @@
"since": null,
"flag": null,
"docs": "docs/PET_MODE.md",
"registryLine": 630
"registryLine": 649
},
{
"id": "overview",
@@ -841,7 +867,7 @@
"since": null,
"flag": null,
"docs": "docs/PROVIDERS.md",
"registryLine": 639
"registryLine": 658
},
{
"id": "account-login",
@@ -854,7 +880,7 @@
"since": null,
"flag": null,
"docs": null,
"registryLine": 648
"registryLine": 667
},
{
"id": "account-computer-files",
@@ -867,7 +893,7 @@
"since": "0.10.2",
"flag": null,
"docs": null,
"registryLine": 656
"registryLine": 675
},
{
"id": "context-anchors",
@@ -880,7 +906,7 @@
"since": null,
"flag": null,
"docs": null,
"registryLine": 665
"registryLine": 684
},
{
"id": "window-pin",
@@ -893,7 +919,7 @@
"since": null,
"flag": null,
"docs": null,
"registryLine": 673
"registryLine": 692
},
{
"id": "structured-copy",
@@ -906,7 +932,7 @@
"since": null,
"flag": null,
"docs": null,
"registryLine": 681
"registryLine": 700
},
{
"id": "live-thinking",
@@ -919,7 +945,7 @@
"since": null,
"flag": null,
"docs": null,
"registryLine": 689
"registryLine": 708
},
{
"id": "workbar",
@@ -932,7 +958,7 @@
"since": null,
"flag": null,
"docs": null,
"registryLine": 697
"registryLine": 716
},
{
"id": "terminal-dock",
@@ -945,7 +971,7 @@
"since": "0.10.2",
"flag": null,
"docs": null,
"registryLine": 705
"registryLine": 724
},
{
"id": "telemetry",
@@ -958,7 +984,7 @@
"since": null,
"flag": null,
"docs": "docs/TELEMETRY.md",
"registryLine": 716
"registryLine": 735
},
{
"id": "docker",
@@ -971,7 +997,7 @@
"since": null,
"flag": null,
"docs": "docs/DOCKER.md",
"registryLine": 725
"registryLine": 744
},
{
"id": "termux",
@@ -984,7 +1010,7 @@
"since": null,
"flag": null,
"docs": "docs/TERMUX.md",
"registryLine": 734
"registryLine": 753
},
{
"id": "chatgpt-plan-sign-in",
@@ -999,7 +1025,7 @@
"since": "0.10.1",
"flag": null,
"docs": "docs/PROVIDERS.md#sign-in-with-chatgpt",
"registryLine": 743
"registryLine": 762
},
{
"id": "claude-plan-sign-in",
@@ -1014,7 +1040,7 @@
"since": "0.10.2",
"flag": null,
"docs": "docs/PROVIDERS.md#sign-in-with-claude",
"registryLine": 753
"registryLine": 772
},
{
"id": "plugin-oauth-providers",
@@ -1029,7 +1055,7 @@
"since": "0.10.1",
"flag": null,
"docs": "docs/PLUGIN_PROVIDERS.md",
"registryLine": 763
"registryLine": 782
},
{
"id": "orcarouter-sign-in",
@@ -1044,7 +1070,7 @@
"since": "0.10.1",
"flag": null,
"docs": "docs/PROVIDERS.md",
"registryLine": 773
"registryLine": 792
},
{
"id": "merch-interest",
@@ -1059,7 +1085,7 @@
"since": "unreleased",
"flag": null,
"docs": "web/lib/merch/README.md",
"registryLine": 784
"registryLine": 803
},
{
"id": "profile-constitution",
@@ -1073,7 +1099,7 @@
"since": "0.10.1",
"flag": null,
"docs": "docs/RUNTIME_API.md#profile-constitution",
"registryLine": 794
"registryLine": 813
},
{
"id": "plugin-shell-install",
@@ -1086,7 +1112,7 @@
"since": "0.10.2",
"flag": null,
"docs": "docs/PLUGINS.md",
"registryLine": 804
"registryLine": 823
},
{
"id": "herdr-agent-state",
@@ -1099,7 +1125,7 @@
"since": "0.10.2",
"flag": null,
"docs": null,
"registryLine": 814
"registryLine": 833
},
{
"id": "instruction-budgets",
@@ -1112,7 +1138,7 @@
"since": "unreleased",
"flag": null,
"docs": "docs/CONFIGURATION.md#instruction-budget-instruction_budget-6526",
"registryLine": 823
"registryLine": 842
},
{
"id": "github-action-review",
@@ -1125,7 +1151,7 @@
"since": "unreleased",
"flag": null,
"docs": "docs/GITHUB_ACTION.md",
"registryLine": 833
"registryLine": 852
},
{
"id": "telegram-agent",
@@ -1138,7 +1164,7 @@
"since": "unreleased",
"flag": null,
"docs": "integrations/telegram-bridge/README.md",
"registryLine": 843
"registryLine": 862
},
{
"id": "dingtalk-agent",
@@ -1151,7 +1177,7 @@
"since": "unreleased",
"flag": null,
"docs": "integrations/dingtalk-bridge/README.md",
"registryLine": 853
"registryLine": 872
}
]
}
+3
View File
@@ -203,6 +203,9 @@
"screenshotAlt": "Codewhale v{version}: a captured terminal view with the conversation, message composer and session controls.",
"latestRelease": "Latest release {tag}",
"releaseUnavailable": "Release status unavailable",
"proofContributors": "{count} contributors",
"proofTools": "{count} built-in tools",
"proofProviders": "{count} provider routes",
"currentSource": "Source",
"sourceCandidate": "Unreleased",
"publishedRelease": "released",
+3
View File
@@ -203,6 +203,9 @@
"screenshotAlt": "Codewhale v{version} 的终端实录画面,包含对话、消息输入框与会话控制。",
"latestRelease": "最新发布 {tag}",
"releaseUnavailable": "发布状态暂不可用",
"proofContributors": "{count} 位贡献者",
"proofTools": "{count} 个内置工具",
"proofProviders": "{count} 条服务商路由",
"currentSource": "源码",
"sourceCandidate": "未发布",
"publishedRelease": "已发布",
+2 -1
View File
@@ -5,13 +5,14 @@ export const RELEASE_HIGHLIGHTS = {
comingLede: { en: "Merged on main and not in a published release yet. Build main to try it now.", zh: "已合并到 main,尚未包含在已发布版本中。现在可从 main 构建试用。" },
changelogLink: { en: "Read the changelog", zh: "查看更新日志" },
items: [
{ title: "Steer mid-turn", body: { en: "Ctrl+Enter closes the in-flight model request and continues the same turn with your message. Tab and Shift+Tab change mode and permissions while the agent works; the change applies at its next step.", zh: "Ctrl+Enter 会立即结束正在进行的模型请求,并带着你的消息继续同一轮。智能体工作时也可以用 Tab 和 Shift+Tab 切换模式和权限,改动会在它的下一步生效。" } },
{ title: "MCP", body: { en: "MCP servers now connect through the official MCP SDK (2.4) behind Codewhale's Rust broker. Newer servers are discovered with server/discover; older stdio and HTTP servers fall back to the legacy handshake.", zh: "MCP 服务器现在通过官方 MCP SDK(2.4)连接,由 Codewhale 的 Rust 代理把关。较新的服务器通过 server/discover 发现;较旧的 stdio 和 HTTP 服务器回退到旧版握手。" } },
{ title: "MCP fixes", body: { en: "Older servers that reject the discovery probe still connect. A server that refuses sign-in shows its real 401 and the /mcp login hint, and one whose command cannot start says why.", zh: "拒绝发现探测的旧服务器仍可连接。拒绝登录的服务器会显示真实的 401 和 /mcp login 提示;命令无法启动的服务器会说明原因。" } },
{ title: "DeepSeek Harness plugins", body: { en: "Reviewed DSH plugins run in the Native extension host. Tool output is checked against its declared schema, and unsupported options are refused instead of ignored.", zh: "经过审核的 DSH 插件在 Native 扩展宿主中运行。工具输出按声明的 schema 校验,不支持的选项会被拒绝而不是被忽略。" } },
{ title: "Computer Use", body: { en: "No longer bundled in the binary. Install it from the codewhale marketplace, which points at its own release; every decision still needs your approval.", zh: "不再内置于二进制文件中。可从 codewhale 插件市场安装,市场指向它的独立发布版本;每个决定仍需你批准。" } },
{ title: "Image-heavy sessions", body: { en: "Older images are left out of an outbound request with a receipt, and an HTTP 413 is retried once with a smaller body. Saved images stay intact.", zh: "较早的图片会从发出的请求中省略并留下记录;遇到 HTTP 413 时会用更小的请求体重试一次。已保存的图片保持不变。" } },
{ title: "Instruction budgets", body: { en: "Global and project instructions get separate budgets, and every trim, skip or refusal shows in /context report.", zh: "全局指令和项目指令分别拥有各自的预算,每次截断、跳过或拒绝都会显示在 /context report 中。" } },
{ title: "codewhale uninstall", body: { en: "On Unix, previews the release files the installer recorded; --yes removes only those. Your data and package-managed installs are kept.", zh: "在 Unix 上会预览安装程序记录的发布文件;--yes 只删除这些文件。你的数据和包管理器安装的版本会保留。" } },
{ title: "codewhale uninstall", body: { en: "Previews the release files the installer recorded, on Unix and Windows; --yes removes only those. Package-managed copies print their own removal command. Your data stays unless you add --purge-data. Windows removal is compile-checked but not yet exercised on a Windows host.", zh: "在 Unix 和 Windows 上预览安装程序记录的发布文件;--yes 只删除这些文件。包管理器安装的版本会给出各自的卸载命令。除非加上 --purge-data,否则你的数据会保留。Windows 上的删除已通过编译检查,但尚未在 Windows 主机上实际运行。" } },
{ title: "/rc", body: { en: "codewhale rc attaches to the hosted control plane for web Chat again: it accepts the exact released runtime identity instead of only the retired 0.9.13 one.", zh: "codewhale rc 可以再次连接托管控制平面以使用网页 Chat:它接受确切的已发布运行时身份,而不再只接受已退役的 0.9.13。" } },
],
};
+7 -1
View File
@@ -38,7 +38,7 @@ export interface RepoFacts {
}
export const FACTS: RepoFacts = {
"generatedAt": "2026-10-26T12:11:28.928Z",
"generatedAt": "2026-10-27T22:46:48.361Z",
"sourceRevision": null,
"sourceCommittedAt": null,
"version": "0.10.2",
@@ -53,10 +53,12 @@ export const FACTS: RepoFacts = {
"commands",
"config",
"core",
"dependencies",
"execpolicy",
"hooks",
"lane",
"localization",
"lsp",
"mcp",
"memory",
"models",
@@ -66,14 +68,18 @@ export const FACTS: RepoFacts = {
"paths",
"protocol",
"release",
"repl",
"runtime",
"sandbox",
"sanitize",
"secrets",
"skill-install",
"snapshot",
"state",
"telemetry",
"tools",
"tui",
"util",
"workflow",
"workflow-js"
],
+1 -1
View File
@@ -8,7 +8,7 @@ export interface Feature {
name: string;
summary: string;
status: FeatureStatus;
surfaces: Partial<Record<"tui" | "api" | "web", SurfaceStatus>>;
surfaces: Partial<Record<"tui" | "desktop" | "web" | "api", SurfaceStatus>>;
since: string | null;
flag: string | null;
docs: string | null;
+3
View File
@@ -15,6 +15,9 @@ export const home: HomeDict = {
"Codewhale v{version}: لقطة طرفية تعرض المحادثة وحقل الرسالة وعناصر التحكم في الجلسة.",
latestRelease: "أحدث إصدار {tag}",
releaseUnavailable: "حالة الإصدار غير متاحة",
proofContributors: "{count} مساهمًا",
proofTools: "{count} أداة مدمجة",
proofProviders: "{count} مسار مزوّد",
currentSource: "المصدر",
sourceCandidate: "غير منشور",
publishedRelease: "منشور",
+3
View File
@@ -15,6 +15,9 @@ export const home: HomeDict = {
"Codewhale v{version}: captura del terminal amb la conversa, el camp de missatge i els controls de sessió.",
latestRelease: "Última versió {tag}",
releaseUnavailable: "Estat de la versió no disponible",
proofContributors: "{count} col·laboradors",
proofTools: "{count} eines integrades",
proofProviders: "{count} rutes de proveïdor",
currentSource: "Font",
sourceCandidate: "Sense publicar",
publishedRelease: "publicada",
+3
View File
@@ -15,6 +15,9 @@ export const home: HomeDict = {
"Codewhale v{version}: eine Terminalaufnahme mit Gespräch, Nachrichteneingabe und Sitzungssteuerung.",
latestRelease: "Aktuellstes Release {tag}",
releaseUnavailable: "Release-Status nicht verfügbar",
proofContributors: "{count} Mitwirkende",
proofTools: "{count} integrierte Werkzeuge",
proofProviders: "{count} Anbieter-Routen",
currentSource: "Quelle",
sourceCandidate: "Unveröffentlicht",
publishedRelease: "veröffentlicht",
+3
View File
@@ -17,6 +17,9 @@ export const home: HomeDict = {
latestRelease: "Latest release {tag}",
releaseUnavailable: "Release status unavailable",
proofContributors: "{count} contributors",
proofTools: "{count} built-in tools",
proofProviders: "{count} provider routes",
currentSource: "Source",
sourceCandidate: "Unreleased",
publishedRelease: "released",
+3
View File
@@ -15,6 +15,9 @@ export const home: HomeDict = {
"Codewhale v{version}: captura del terminal con la conversación, el campo de mensaje y los controles de sesión.",
latestRelease: "Último lanzamiento {tag}",
releaseUnavailable: "Estado del lanzamiento no disponible",
proofContributors: "{count} colaboradores",
proofTools: "{count} herramientas integradas",
proofProviders: "{count} rutas de proveedor",
currentSource: "Fuente",
sourceCandidate: "Sin publicar",
publishedRelease: "publicado",
+3
View File
@@ -15,6 +15,9 @@ export const home: HomeDict = {
"Codewhale v{version} : capture du terminal avec la conversation, le champ de message et les commandes de session.",
latestRelease: "Dernière version {tag}",
releaseUnavailable: "État des versions indisponible",
proofContributors: "{count} contributeurs",
proofTools: "{count} outils intégrés",
proofProviders: "{count} routes de fournisseur",
currentSource: "Source",
sourceCandidate: "Non publiée",
publishedRelease: "publiée",
+3
View File
@@ -15,6 +15,9 @@ export const home: HomeDict = {
"Codewhale v{version}: बातचीत, संदेश बॉक्स और सत्र नियंत्रण दिखाने वाला टर्मिनल कैप्चर।",
latestRelease: "नवीनतम रिलीज़ {tag}",
releaseUnavailable: "रिलीज़ स्थिति उपलब्ध नहीं",
proofContributors: "{count} योगदानकर्ता",
proofTools: "{count} अंतर्निहित टूल",
proofProviders: "{count} प्रदाता रूट",
currentSource: "सोर्स",
sourceCandidate: "अप्रकाशित",
publishedRelease: "प्रकाशित",
+3
View File
@@ -15,6 +15,9 @@ export const home: HomeDict = {
"Codewhale v{version}: tangkapan terminal berisi percakapan, kolom pesan, dan kontrol sesi.",
latestRelease: "Rilis terbaru {tag}",
releaseUnavailable: "Status rilis tidak tersedia",
proofContributors: "{count} kontributor",
proofTools: "{count} alat bawaan",
proofProviders: "{count} rute penyedia",
currentSource: "Sumber",
sourceCandidate: "Belum dirilis",
publishedRelease: "dirilis",
+3
View File
@@ -15,6 +15,9 @@ export const home: HomeDict = {
"Codewhale v{version}: una cattura del terminale con conversazione, campo del messaggio e controlli della sessione.",
latestRelease: "Ultima release {tag}",
releaseUnavailable: "Stato delle release non disponibile",
proofContributors: "{count} contributori",
proofTools: "{count} strumenti integrati",
proofProviders: "{count} percorsi dei provider",
currentSource: "Sorgente",
sourceCandidate: "Non rilasciata",
publishedRelease: "rilasciata",
+3
View File
@@ -15,6 +15,9 @@ export const home: HomeDict = {
"Codewhale v{version} のターミナル収録画面。会話、メッセージ入力欄、セッション操作を表示。",
latestRelease: "最新リリース {tag}",
releaseUnavailable: "リリース情報を取得できません",
proofContributors: "コントリビューター {count} 人",
proofTools: "組み込みツール {count} 個",
proofProviders: "プロバイダールート {count} 件",
currentSource: "ソース",
sourceCandidate: "未リリース",
publishedRelease: "リリース済み",
+3
View File
@@ -15,6 +15,9 @@ export const home: HomeDict = {
"Codewhale v{version}: 대화, 메시지 입력창, 세션 제어를 보여 주는 실제 터미널 캡처.",
latestRelease: "최신 릴리스 {tag}",
releaseUnavailable: "릴리스 상태를 확인할 수 없음",
proofContributors: "기여자 {count}명",
proofTools: "내장 도구 {count}개",
proofProviders: "제공자 경로 {count}개",
currentSource: "소스",
sourceCandidate: "미공개",
publishedRelease: "공개됨",
+3
View File
@@ -15,6 +15,9 @@ export const home: HomeDict = {
"Codewhale v{version}: zrzut terminala z rozmową, polem wiadomości i kontrolkami sesji.",
latestRelease: "Najnowsze wydanie {tag}",
releaseUnavailable: "Status wydania niedostępny",
proofContributors: "Współtwórcy: {count}",
proofTools: "Wbudowane narzędzia: {count}",
proofProviders: "Trasy dostawców: {count}",
currentSource: "Źródło",
sourceCandidate: "Niewydane",
publishedRelease: "wydane",
+3
View File
@@ -15,6 +15,9 @@ export const home: HomeDict = {
"Codewhale v{version}: captura do terminal com conversa, campo de mensagem e controles da sessão.",
latestRelease: "Último lançamento {tag}",
releaseUnavailable: "Status do lançamento indisponível",
proofContributors: "{count} colaboradores",
proofTools: "{count} ferramentas integradas",
proofProviders: "{count} rotas de provedor",
currentSource: "Código-fonte",
sourceCandidate: "Não publicado",
publishedRelease: "publicado",
+3
View File
@@ -15,6 +15,9 @@ export const home: HomeDict = {
"Codewhale v{version}: снимок терминала с беседой, полем сообщения и управлением сессией.",
latestRelease: "Последний релиз {tag}",
releaseUnavailable: "Статус релиза недоступен",
proofContributors: "Участников: {count}",
proofTools: "Встроенных инструментов: {count}",
proofProviders: "Маршрутов провайдеров: {count}",
currentSource: "Исходники",
sourceCandidate: "Не выпущено",
publishedRelease: "выпущено",
+3
View File
@@ -15,6 +15,9 @@ export const home: HomeDict = {
"Codewhale v{version}: sohbeti, mesaj alanını ve oturum kontrollerini gösteren terminal kaydı.",
latestRelease: "En yeni sürüm {tag}",
releaseUnavailable: "Sürüm durumu kullanılamıyor",
proofContributors: "{count} katkıda bulunan",
proofTools: "{count} yerleşik araç",
proofProviders: "{count} sağlayıcı rotası",
currentSource: "Kaynak",
sourceCandidate: "Yayımlanmadı",
publishedRelease: "yayımlandı",
+3
View File
@@ -162,6 +162,9 @@ export interface HomeDict {
/** "Latest release {tag}" */
latestRelease: string;
releaseUnavailable: string;
proofContributors: string;
proofTools: string;
proofProviders: string;
/** "Source" / "Unreleased" — prepended to `v{version}`. */
currentSource: string;
sourceCandidate: string;
+3
View File
@@ -15,6 +15,9 @@ export const home: HomeDict = {
"Codewhale v{version}: знімок термінала з розмовою, полем повідомлення й керуванням сесією.",
latestRelease: "Останній реліз {tag}",
releaseUnavailable: "Статус релізу недоступний",
proofContributors: "Учасників: {count}",
proofTools: "Вбудованих інструментів: {count}",
proofProviders: "Маршрутів провайдерів: {count}",
currentSource: "Джерело",
sourceCandidate: "Не випущено",
publishedRelease: "випущено",
+3
View File
@@ -15,6 +15,9 @@ export const home: HomeDict = {
"Codewhale v{version}: ảnh chụp terminal với cuộc trò chuyện, ô nhập tin nhắn và điều khiển phiên.",
latestRelease: "Bản phát hành mới nhất {tag}",
releaseUnavailable: "Không có trạng thái phát hành",
proofContributors: "{count} người đóng góp",
proofTools: "{count} công cụ tích hợp",
proofProviders: "{count} tuyến nhà cung cấp",
currentSource: "Mã nguồn",
sourceCandidate: "Chưa phát hành",
publishedRelease: "đã phát hành",
+3
View File
@@ -15,6 +15,9 @@ export const home: HomeDict = {
"Codewhale v{version} 的终端实录画面,包含对话、消息输入框与会话控制。",
latestRelease: "最新发布 {tag}",
releaseUnavailable: "发布状态暂不可用",
proofContributors: "{count} 位贡献者",
proofTools: "{count} 个内置工具",
proofProviders: "{count} 条服务商路由",
currentSource: "源码",
sourceCandidate: "未发布",
publishedRelease: "已发布",
+7 -1
View File
@@ -41,7 +41,13 @@ export const RELEASE_CONTRIBUTORS: string[] = [
* one. scripts/check-contributor-credit.py requires them here now; the release
* cut moves them into RELEASE_CONTRIBUTORS with that release's changelog block.
*/
export const UNRELEASED_CONTRIBUTORS: string[] = [];
export const UNRELEASED_CONTRIBUTORS: string[] = [
"@7jrxt42BxFZo4iAnN4CX",
"@Lstarsky0",
"@gaord",
"@SparkofSpike",
"@Aboysky",
];
/**
* Contributors who helped with reports, reproductions, and verification.
+1 -1
View File
@@ -160,7 +160,7 @@ export function deriveFeatures() {
}
ids.add(row.id);
for (const [surface, state] of Object.entries(row.surfaces)) {
if (!["tui", "api", "web"].includes(surface) || !["stable", "partial", "preview", "none"].includes(state)) {
if (!["tui", "desktop", "web", "api"].includes(surface) || !["stable", "partial", "preview", "none"].includes(state)) {
throw new Error(`invalid feature surface: ${row.id}/${surface}`);
}
}