fix(session): preserve stamped turns and shared journal ancestors (#7180) [skip ci]

Keep the contributor's original commits and their Windows Console feature fix.
A stamp enriches an unshared user entry. Preserve authored payload, cache
markers, runtime ownership, conflicting stamps and preexisting branch history.
Use the same prefix semantics for saved outcomes and TUI refresh. Record
the strictly changed byte offset separately for token accounting.

42 focused tests passed. The macOS PTY sent the same text twice on purpose,
one Enter at a time. The loopback provider saw exactly two requests, the two
answers stayed on the active path, and it matched the persisted transcript.
The linked Windows incident has not been reproduced. Refs #7178.
This commit is contained in:
CodeWhale Bot committed 2026-10-31 22:25:13 -07:00
commit c80c16c14a
36 files changed
+856 -326

No files matched your search

+8 -2
View File
@@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- **Composition on the first request:** `execute_tools` accepts a bounded named batch from the current MCP catalog, with argument names from its allowed members and the same nested gate as a JavaScript program. Exact tool-name searches work with regex anchors. Stream receipts hash the executable in the background; `CODEWHALE_TIMING=1` reports phase durations. (CW-7649)
- **The model can read what compaction replaced (#7173):** two read-only tools,
`context_archive_search` and `context_archive_get`, search the session's
saved pre-compaction snapshots and read one archived message by id, with
@@ -28,7 +29,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **Runcrate is a bundled provider:** `runcrate` reaches the OpenAI-compatible host at `https://api.runcrate.ai/v1` with `RUNCRATE_API_KEY`, and model ids come from its public `GET /v1/models` listing. Thanks @heydryft for the request and the vendor details. (#7085)
- **MCP connection status reaches the model:** one existing history snapshot now records permitted configured, connecting, failed, authorization-required, unknown and connected servers, with bounded inert names and no raw errors. Stale status and server guidance are withdrawn on contention, configuration/scope changes and restore; observation does not connect servers or promise callable tools. (#6866)
- **Bundled-skill policy:** `[skills.bundled]` separates automatic, authoritative and disabled installation from discovery, with per-name selection, disable and explicit-only controls. Aliases share policy; disabled shipped copies reveal user alternatives. Modified and managed files remain intact, and doctor plus `/skills inspect` show the effective policy. (#7066)
- **Configurable skill routing index:** `[skills] listing_max_description_chars` and `listing_budget_fraction` control the shared parent/child catalogue. Defaults stay 400 characters and 5% of the route window, within the existing total bounds. Load warnings, localized inspect notes and `/context` explain shortening; full skill loading stays available. (#7073)
- **Configurable skill routing index:** `[skills] listing_max_description_chars` and `listing_budget_fraction` control the shared parent/child catalogue. Defaults use 120 characters and 0.2% of the route window, within the existing total bounds. Load warnings, localized inspect notes and `/context` explain shortening; full skill loading stays available. (#7073)
- **Compact approvals keep their authority visible:** cards show three choices and default to Deny. An exact saved rule requires its scope preview on screen. Shortcuts for additional actions require opening Details; a hidden saved-rule choice never falls back to granting the session.
- **Quicker everyday navigation:** top-level help separates everyday and advanced commands, `doctor --all` expands path/backend details, and the launch screen labels how to resume recent work. Up selects the newest recent session. Whale cameos stay out of transcript text.
@@ -157,6 +158,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
- Saving a user turn when the Engine adds its metadata preserves the journal entry and its place in history. Sessions with a fork keep the earlier branch intact. Thanks [@SparkofSpike](https://github.com/SparkofSpike). ([#7180](https://github.com/codewhale-hq/Codewhale/pull/7180))
- **Star keycaps stay intact in Markdown (#7176):** plain and escaped text
preserve the full grapheme, and a keycap cannot open emphasis. The separate
Windows Terminal font-fallback issue remains unqualified. Thanks
[@SparkofSpike](https://github.com/SparkofSpike).
- **Codewhale runs from a HarmonyOS user directory (#7168):** on HarmonyOS hmdfs (the 2in1 `/storage/Users/currentUser` mount), which reports its own owner and a group-writable mode for every object and refuses `chmod` and advisory locks, a GUI start failed one feature at a time — the workspace lease, the owner control socket, then the OAuth credential directory. Private directories, the secret store and credential files now probe the mount once, keep the strict ownership check everywhere it can be expressed, and on a mount that cannot express ownership still enforce no-follow, single-link and atomic publication, with one warning naming what the mount reported. A filesystem without advisory locks serialises secret-store writes within the process instead of refusing; a held lock is still contention. Not observed here on a real HarmonyOS device. Thanks [@gaord](https://github.com/gaord).
- **Notes stop swallowing a backslash:** a transcript note is machine-generated and names paths, but it went through the markdown renderer, where CommonMark §2.4 reads a backslash before ASCII punctuation as an escape. A note naming `C:\Users\asus\.codewhale\setup_state.json` therefore printed as `C:\Users\asus.codewhale\setup_state.json` — a directory that does not exist, in the one line a user would copy to find the real one. Notes are now escaped where they are rendered. Fenced blocks, and lines carrying inline code or a table row, are passed through untouched, because the renderer already copies those verbatim and doubling a backslash inside one would print it doubled.
- **Setup state survives a second instance:** two running Codewhale windows share one home, and both rewrite `setup_state.json` when the provider or model changes. The replacing rename is now retried briefly when the destination is held open for a read — Windows refuses it outright while that handle lives — instead of surfacing a write the caller could have made. The failure message also carries the underlying I/O error instead of only the outer context, so the next occurrence is diagnosable.
@@ -338,7 +344,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **[@gaord](https://github.com/gaord)** — a running turn's cost and context in the Runtime ([#7074](https://github.com/codewhale-hq/Codewhale/pull/7074)) forks that measure the history they carry ([#7147](https://github.com/codewhale-hq/Codewhale/pull/7147)), and private state on a HarmonyOS mount that cannot express ownership ([#7168](https://github.com/codewhale-hq/Codewhale/pull/7168)).
- **[@luochen211](https://github.com/luochen211)** — one form per term in the Simplified Chinese pack ([#7150](https://github.com/codewhale-hq/Codewhale/pull/7150)) and dropped blanket dead-code allows in fleet alerts and the pet worker ([#7149](https://github.com/codewhale-hq/Codewhale/pull/7149)).
- **[@gannp](https://github.com/gannp)** — reported vLLM effort values that ignored the local model template and fixed it with a reviewed template registry ([#7124](https://github.com/codewhale-hq/Codewhale/issues/7124), [#7125](https://github.com/codewhale-hq/Codewhale/pull/7125)).
- **[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)) and made a goal pause to ask instead of inventing the answer ([#7175](https://github.com/codewhale-hq/Codewhale/pull/7175)), and gave the model a way back into the transcript compaction replaced ([#7173](https://github.com/codewhale-hq/Codewhale/pull/7173)).
- **[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)) and made a goal pause to ask instead of inventing the answer ([#7175](https://github.com/codewhale-hq/Codewhale/pull/7175)), and gave the model a way back into the transcript compaction replaced ([#7173](https://github.com/codewhale-hq/Codewhale/pull/7173)). Also preserved journal history when a provisional input receives its Engine metadata ([#7180](https://github.com/codewhale-hq/Codewhale/pull/7180)).
- **[@ddlw](https://github.com/ddlw)** — repaired main for the 0.10.3 batch: clippy `-D warnings` across protocol, runtime, util and tui (boxed review and MCP-add payloads, scoped dead-code expectations, cfg-gated platform test helpers), rustfmt, split extension-host imports, the provider-registry identity check, Windows-stable DSH receipt bytes, and the first-party marketplace receipt ([#7108](https://github.com/codewhale-hq/Codewhale/pull/7108), [#7109](https://github.com/codewhale-hq/Codewhale/pull/7109)).
- **[@laozidabo](https://github.com/laozidabo)** — localized `/diff` feedback ([#7101](https://github.com/codewhale-hq/Codewhale/pull/7101)), flushed queued saves before signal exit ([#7122](https://github.com/codewhale-hq/Codewhale/pull/7122)) and kept `/rc` start failures visible ([#7127](https://github.com/codewhale-hq/Codewhale/pull/7127)).
- **[Boyce / @Aboysky](https://github.com/Aboysky)** — refreshed the README translation source stamps ([#7104](https://github.com/codewhale-hq/Codewhale/pull/7104)) and localized `/undo` feedback ([#7121](https://github.com/codewhale-hq/Codewhale/pull/7121)).
+77 -43
View File
@@ -2108,6 +2108,7 @@ impl Engine {
mut host: Option<EngineHostSetup>,
supplied_subagent_manager: Option<SharedSubAgentManager>,
) -> (Self, EngineHandle) {
let _timing = codewhale_runtime::logging::PhaseTimer::start("engine_init");
codewhale_release::tls::ensure_rustls_crypto_provider();
let mut instruction_settings =
crate::project_context::InstructionSettings::from_config(api_config);
@@ -6502,6 +6503,7 @@ impl Engine {
autonomous: bool,
review: Option<codewhale_protocol::op::ReviewOperation>,
) -> SendMessageOutcome {
let setup_timing = codewhale_runtime::logging::PhaseTimer::start("turn_setup");
let is_review = review.is_some();
let TurnSpec {
profile_constitution,
@@ -7004,6 +7006,7 @@ impl Engine {
// The label carries a truncated first line of the prompt so
// `/restore` listings are human-readable.
if !is_review {
let _timing = codewhale_runtime::logging::PhaseTimer::start("workspace_snapshot");
self.take_restore_point(
WorkspaceSnapshotKind::PreTurn,
format_snapshot_label("pre-turn", self.turn_counter, Some(&content)),
@@ -7265,6 +7268,7 @@ impl Engine {
// Build tool registry and tool list for the current mode
let turn_id_for_mailbox = turn.id.clone();
let catalog_timing = codewhale_runtime::logging::PhaseTimer::start("turn_catalog");
let TurnToolBuild {
surface,
mailbox: mailbox_for_runtime,
@@ -7293,6 +7297,7 @@ impl Engine {
&turn_id_for_mailbox,
)
.await;
drop(catalog_timing);
let tool_catalog_for_event = Some(surface.catalog.clone());
// Resolve, once per turn, the out-of-request facts the read-only
@@ -7337,6 +7342,7 @@ impl Engine {
let foreground_children_for_turn = mailbox_for_runtime
.as_ref()
.map(|barrier| Arc::clone(&barrier.foreground_children));
drop(setup_timing);
let turn_result = std::panic::AssertUnwindSafe(async {
if let Some(request) = review {
return self.run_structured_review(&mut turn, request).await;
@@ -7361,6 +7367,7 @@ impl Engine {
// Every return path (including a caught panic) leaves the phase idle,
// so the stall watchdog never reports a turn that already ended.
self.turn_heartbeat.idle();
let _post_timing = codewhale_runtime::logging::PhaseTimer::start("turn_postprocessing");
let (mut status, error) = match turn_result {
Ok(outcome) => outcome,
Err(panic) => {
@@ -9317,56 +9324,83 @@ impl Engine {
/// Both share one nonblocking pool capture, current turn ceiling and
/// authority check. Empty captures withdraw previous notes, including
/// after restore, contention, configuration changes or scope removal.
async fn record_mcp_runtime_context(&mut self, policy: &ToolSurfacePolicy, catalog: &[Tool]) {
async fn record_mcp_runtime_context(
&mut self,
policy: &ToolSurfacePolicy,
catalog: &mut [Tool],
) {
let _timing = codewhale_runtime::logging::PhaseTimer::start("mcp_context");
let eligible = self.child_host.is_none()
&& self.rlm_host.is_none()
&& !self.is_acp_turn()
&& !self.api_config.runtime_chat_isolated
&& self.config.features.enabled(Feature::Mcp);
let (servers, instructions) = match self.mcp_pool.as_ref().filter(|_| eligible).cloned() {
Some(pool) => {
let visible: HashSet<String> = catalog
.iter()
.filter(|tool| crate::mcp::McpPool::is_mcp_tool(&tool.name))
.filter(|tool| {
!policy.denies_tool(&tool.name) && policy.passes_allow_list(&tool.name)
})
.map(|tool| tool.name.clone())
.collect();
let discovery_available = catalog.iter().any(|tool| {
is_tool_search_tool(&tool.name)
&& !policy.denies_tool(&tool.name)
&& policy.passes_allow_list(&tool.name)
});
let permitted = policy.mcp_discovery_filter();
let failed: HashSet<String> = self.mcp_connection_errors.keys().cloned().collect();
tokio::task::spawn_blocking(move || {
let Ok(pool) = pool.try_lock() else {
return (Vec::new(), Vec::new());
};
if !pool.config_sources_unchanged() {
return (Vec::new(), Vec::new());
}
let servers = pool.model_server_statuses(
&permitted,
&visible,
discovery_available,
&failed,
);
let mut instructions =
pool.model_server_instructions(|tool| visible.contains(tool));
instructions.retain(|(name, _)| {
servers.iter().any(|(server, status)| {
server == name && *status == codewhale_mcp::ModelServerStatus::Connected
let (servers, instructions, batch, members) =
match self.mcp_pool.as_ref().filter(|_| eligible).cloned() {
Some(pool) => {
let visible: HashSet<String> = catalog
.iter()
.filter(|tool| crate::mcp::McpPool::is_mcp_tool(&tool.name))
.filter(|tool| {
!policy.denies_tool(&tool.name) && policy.passes_allow_list(&tool.name)
})
.map(|tool| tool.name.clone())
.collect();
let discovery_available = catalog.iter().any(|tool| {
is_tool_search_tool(&tool.name)
&& !policy.denies_tool(&tool.name)
&& policy.passes_allow_list(&tool.name)
});
(servers, instructions)
})
.await
.unwrap_or_default()
}
None => (Vec::new(), Vec::new()),
};
let composition = (policy.mode != AppMode::Plan
&& tool_catalog::composition_eager(catalog))
.then(|| tool_catalog::unique_named_batch(catalog))
.flatten()
.map(|index| catalog[index].name.clone());
let permitted = policy.mcp_discovery_filter();
let failed: HashSet<String> =
self.mcp_connection_errors.keys().cloned().collect();
tokio::task::spawn_blocking(move || {
let Ok(pool) = pool.try_lock() else {
return Default::default();
};
if !pool.config_sources_unchanged() {
return Default::default();
}
let routes = pool.resolved_tool_servers();
let servers = pool.model_server_statuses_for(
&permitted,
&visible,
discovery_available,
&failed,
&routes,
);
let composition = composition.filter(|batch| {
routes.get(batch).is_some_and(|owner| {
servers.iter().any(|(name, status)| {
name == owner
&& *status == codewhale_mcp::ModelServerStatus::Connected
})
})
});
let (mut instructions, members) = pool.model_server_instructions_for(
|tool| visible.contains(tool),
composition.as_deref(),
&routes,
);
instructions.retain(|(name, _)| {
servers.iter().any(|(server, status)| {
server == name
&& *status == codewhale_mcp::ModelServerStatus::Connected
})
});
(servers, instructions, composition, members)
})
.await
.unwrap_or_default()
}
None => Default::default(),
};
tool_catalog::configure_composition_entry(catalog, batch.as_deref(), &members);
let previous =
self.session.messages.iter().rev().find(|message| {
crate::runtime_handoff::is_mcp_server_instructions_message(message)
@@ -238,6 +238,7 @@ impl Engine {
event: Event,
withdraw: Option<&CancellationToken>,
) -> Result<ApprovalResult, ToolError> {
let _timing = codewhale_runtime::logging::PhaseTimer::start("approval_wait");
let ask = match &event {
Event::ApprovalRequired {
input, description, ..
@@ -519,7 +519,7 @@ fn tool_search_scenario() {
let result = execute_tool_search(
TOOL_SEARCH_NAME,
&json!({"query":"ask user question"}),
&json!({"query":format!("^{REQUEST_USER_INPUT_NAME}$"),"match":"regex"}),
&catalog,
&mut active,
)
@@ -1191,10 +1191,10 @@ async fn mcp_server_instructions_reach_the_request_labelled_once_and_only_for_vi
engine.mcp_pool = Some(Arc::new(AsyncMutex::new(pool)));
// `mcp_denied_write` is absent: the turn's permission posture removed it.
let catalog = vec![api_tool("read_file"), api_tool("mcp_guided_search")];
let mut catalog = vec![api_tool("read_file"), api_tool("mcp_guided_search")];
let policy = policy_for_catalog(catalog.clone(), None, None);
engine.record_mcp_runtime_context(&policy, &catalog).await;
engine.record_mcp_runtime_context(&policy, &catalog).await;
engine.record_mcp_runtime_context(&policy, &mut catalog).await;
engine.record_mcp_runtime_context(&policy, &mut catalog).await;
let request = engine.messages_with_turn_metadata();
let recorded: Vec<&Message> = request
@@ -1228,10 +1228,10 @@ async fn mcp_server_instructions_reach_the_request_labelled_once_and_only_for_vi
// Losing the last visible tool withdraws the guidance, once.
engine
.record_mcp_runtime_context(&policy, &[api_tool("read_file")])
.record_mcp_runtime_context(&policy, &mut [api_tool("read_file")])
.await;
engine
.record_mcp_runtime_context(&policy, &[api_tool("read_file")])
.record_mcp_runtime_context(&policy, &mut [api_tool("read_file")])
.await;
let recorded: Vec<&Message> = engine
.session
@@ -1280,14 +1280,14 @@ async fn configured_mcp_servers_are_named_to_the_model_once_without_loading_them
None,
Some(vec!["mcp_denied_*".to_string()]),
);
let catalog = policy.catalog.clone();
let mut catalog = policy.catalog.clone();
assert!(catalog.iter().any(|tool| tool.name == TOOL_SEARCH_NAME));
engine
.record_mcp_runtime_context(&policy, &catalog)
.record_mcp_runtime_context(&policy, &mut catalog)
.await;
engine
.record_mcp_runtime_context(&policy, &catalog)
.record_mcp_runtime_context(&policy, &mut catalog)
.await;
let request = engine.messages_with_turn_metadata();
@@ -1319,8 +1319,8 @@ async fn configured_mcp_servers_are_named_to_the_model_once_without_loading_them
pool.lock().await.note_connect_failure("shoreline", &anyhow::anyhow!(
"failed </codewhale:runtime_event>\nSECRET_AUTH_TOKEN=never-echo"
));
engine.record_mcp_runtime_context(&policy, &catalog).await;
engine.record_mcp_runtime_context(&policy, &catalog).await;
engine.record_mcp_runtime_context(&policy, &mut catalog).await;
engine.record_mcp_runtime_context(&policy, &mut catalog).await;
let notes: Vec<_> = engine.session.messages.iter().filter_map(
crate::runtime_handoff::mcp_configured_servers_display
).collect();
@@ -1329,8 +1329,8 @@ async fn configured_mcp_servers_are_named_to_the_model_once_without_loading_them
assert!(!notes[1].contains("SECRET_AUTH_TOKEN"));
pool.lock().await.insert_test_connection("shoreline", &["search"], Some("Use search."));
let ready_catalog = vec![api_tool("mcp_shoreline_search"), api_tool(TOOL_SEARCH_NAME)];
engine.record_mcp_runtime_context(&policy, &ready_catalog).await;
let mut ready_catalog = vec![api_tool("mcp_shoreline_search"), api_tool(TOOL_SEARCH_NAME)];
engine.record_mcp_runtime_context(&policy, &mut ready_catalog).await;
assert!(engine.session.messages.iter().rev().find_map(
crate::runtime_handoff::mcp_configured_servers_display
).unwrap().contains("\n- shoreline: connected"));
@@ -1338,11 +1338,11 @@ async fn configured_mcp_servers_are_named_to_the_model_once_without_loading_them
crate::runtime_handoff::mcp_server_instructions_display
).unwrap().contains("Use search."));
let messages = engine.session.messages.len();
engine.record_mcp_runtime_context(&policy, &ready_catalog).await;
engine.record_mcp_runtime_context(&policy, &mut ready_catalog).await;
assert_eq!(engine.session.messages.len(), messages);
let held = pool.lock().await;
engine.record_mcp_runtime_context(&policy, &ready_catalog).await;
engine.record_mcp_runtime_context(&policy, &mut ready_catalog).await;
assert!(engine.session.messages.iter().rev().find_map(
crate::runtime_handoff::mcp_configured_servers_display
).unwrap().contains("no longer applies"));
@@ -1350,7 +1350,7 @@ async fn configured_mcp_servers_are_named_to_the_model_once_without_loading_them
crate::runtime_handoff::mcp_server_instructions_display
).unwrap().contains("no longer applies"));
drop(held);
engine.record_mcp_runtime_context(&policy, &ready_catalog).await;
engine.record_mcp_runtime_context(&policy, &mut ready_catalog).await;
assert!(engine.session.messages.iter().rev().find_map(
crate::runtime_handoff::mcp_configured_servers_display
).unwrap().contains("\n- shoreline: connected"));
@@ -1360,7 +1360,7 @@ async fn configured_mcp_servers_are_named_to_the_model_once_without_loading_them
&& crate::runtime_handoff::mcp_server_instructions_display(message).is_none()
}).cloned().collect();
engine.session.replace_messages(retained);
engine.record_mcp_runtime_context(&policy, &ready_catalog).await;
engine.record_mcp_runtime_context(&policy, &mut ready_catalog).await;
assert_eq!(engine.session.messages.iter().filter_map(
crate::runtime_handoff::mcp_configured_servers_display
).count(), 1);
@@ -1368,7 +1368,7 @@ async fn configured_mcp_servers_are_named_to_the_model_once_without_loading_them
vec![api_tool("mcp_shoreline_search")],
Some(vec!["mcp_shoreline_search".to_owned()]), None,
);
engine.record_mcp_runtime_context(&selected, &[api_tool("mcp_shoreline_search")]).await;
engine.record_mcp_runtime_context(&selected, &mut [api_tool("mcp_shoreline_search")]).await;
let latest = engine.session.messages.iter().rev().find_map(
crate::runtime_handoff::mcp_configured_servers_display
).unwrap();
@@ -1376,13 +1376,13 @@ async fn configured_mcp_servers_are_named_to_the_model_once_without_loading_them
assert!(!latest.contains("forged_"));
engine.api_config.runtime_chat_isolated = true;
engine.record_mcp_runtime_context(&policy, &ready_catalog).await;
engine.record_mcp_runtime_context(&policy, &mut ready_catalog).await;
assert!(engine.session.messages.iter().rev().find_map(
crate::runtime_handoff::mcp_configured_servers_display
).unwrap().contains("no longer applies"));
engine.api_config.runtime_chat_isolated = false;
engine.session.messages.clear();
engine.record_mcp_runtime_context(&policy, &ready_catalog).await;
engine.record_mcp_runtime_context(&policy, &mut ready_catalog).await;
// Without `tool_search` the model could not act on the list: withdrawn, once.
let narrowed = policy_for_catalog(
@@ -1390,12 +1390,12 @@ async fn configured_mcp_servers_are_named_to_the_model_once_without_loading_them
Some(vec!["read_file".to_string()]),
None,
);
let catalog = narrowed.catalog.clone();
let mut catalog = narrowed.catalog.clone();
engine
.record_mcp_runtime_context(&narrowed, &catalog)
.record_mcp_runtime_context(&narrowed, &mut catalog)
.await;
engine
.record_mcp_runtime_context(&narrowed, &catalog)
.record_mcp_runtime_context(&narrowed, &mut catalog)
.await;
let recorded: Vec<&str> = engine
.session
@@ -1419,32 +1419,32 @@ async fn no_configured_mcp_servers_adds_nothing_to_history() {
);
let before = engine.session.messages.len();
let policy = policy_for_catalog(vec![api_tool("read_file")], None, None);
let catalog = policy.catalog.clone();
let mut catalog = policy.catalog.clone();
engine
.record_mcp_runtime_context(&policy, &catalog)
.record_mcp_runtime_context(&policy, &mut catalog)
.await;
engine.mcp_pool = Some(Arc::new(AsyncMutex::new(McpPool::new(
codewhale_mcp::config::McpConfig::default(),
))));
engine
.record_mcp_runtime_context(&policy, &catalog)
.record_mcp_runtime_context(&policy, &mut catalog)
.await;
assert_eq!(engine.session.messages.len(), before);
let path = tmp.path().join("mcp.json");
std::fs::write(&path, r#"{"mcpServers":{"fresh":{"command":"unused"}}}"#).unwrap();
engine.mcp_pool = Some(Arc::new(AsyncMutex::new(McpPool::from_config_path(&path).unwrap())));
engine.record_mcp_runtime_context(&policy, &catalog).await;
engine.record_mcp_runtime_context(&policy, &mut catalog).await;
assert!(engine.session.messages.iter().rev().find_map(
crate::runtime_handoff::mcp_configured_servers_display
).unwrap().contains("\n- fresh: configured"));
std::fs::remove_file(&path).unwrap();
engine.record_mcp_runtime_context(&policy, &catalog).await;
engine.record_mcp_runtime_context(&policy, &mut catalog).await;
assert!(engine.session.messages.iter().rev().find_map(
crate::runtime_handoff::mcp_configured_servers_display
).unwrap().contains("no longer applies"));
engine.config.features.disable(Feature::Mcp);
let count = engine.session.messages.len();
engine.record_mcp_runtime_context(&policy, &catalog).await;
engine.record_mcp_runtime_context(&policy, &mut catalog).await;
assert_eq!(engine.session.messages.len(), count);
}
+106 -4
View File
@@ -265,8 +265,7 @@ pub(super) fn surface_budgets_produce_same_catalog(
/// Mirrors Codex's `ToolMode`: the model's own metadata wins, `[features]`
/// flags override the default, and anything else is [`ToolMode::Direct`].
/// There is no user-facing mode to enter — the catalog shape is the whole
/// mechanism, so `CodeMode` only promotes `execute_tools` from deferred to
/// eager. (A `CodeModeOnly` restriction needs dispatch enforcement and is a
/// mechanism. Code mode exposes the VM with at most one named batch shortcut. (A `CodeModeOnly` restriction needs dispatch enforcement and is a
/// later slice, not a third variant here.)
///
/// KV-cache effect: the inputs are session config (plus future per-model
@@ -406,6 +405,109 @@ pub fn ensure_advanced_tooling(
}
}
pub(super) fn unique_named_batch(catalog: &[Tool]) -> Option<usize> {
let mut batches = catalog.iter().enumerate().filter(|(_, tool)| {
crate::mcp::McpPool::is_mcp_tool(&tool.name)
&& crate::mcp::named_batch_members(&tool.input_schema).is_some()
});
let (index, tool) = batches.next()?;
(batches.next().is_none()
&& tool.name.len() <= 128
&& tool
.name
.bytes()
.all(|b| b.is_ascii_alphanumeric() || b == b'_' || b == b'-')
&& tool.description.len() <= 2048
&& tool.input_schema.to_string().len() + tool.description.len() <= 8192)
.then_some(index)
}
pub(super) fn composition_eager(catalog: &[Tool]) -> bool {
catalog
.iter()
.any(|t| t.name == EXECUTE_TOOLS_TOOL_NAME && !t.defer_loading.unwrap_or(false))
}
pub(super) fn configure_composition_entry(
catalog: &mut [Tool],
name: Option<&str>,
members: &[String],
) {
if !composition_eager(catalog) {
return;
}
let mut definition = crate::tools::codemode::execute_tools_tool_definition();
if !members.is_empty()
&& let Some(batch) = catalog.iter().find(|tool| Some(tool.name.as_str()) == name)
&& let Some((field, _)) = crate::mcp::named_batch_members(&batch.input_schema)
{
let mut arguments = batch.input_schema.clone();
arguments["properties"][field]["items"]["properties"]["tool"]["enum"] = json!(members);
if !batch.defer_loading.unwrap_or(false) {
arguments = json!({"type":"object", "description":"Use the named batch schema shown in this request; only these step tools are offered here.",
"required":[field], "properties":{(field):{"type":"array","minItems":arguments["properties"][field]["minItems"].as_u64().unwrap_or(1),"maxItems":arguments["properties"][field]["maxItems"],
"items":{"type":"object","required":["tool"],"properties":{"arguments":{"type":"object"},
"tool":{"type":"string","enum":members}}}}}});
}
definition.description.push_str(" For a known sequence, pass batch instead of code. It runs that server's named steps in one gated call and returns a bounded result. Group known steps, including setup and their verification, in one batch. Use code if later actions depend on reading an intermediate result.");
definition.input_schema["properties"]["batch"] = json!({
"type":"object", "description":batch.description,
"required":["tool","arguments"], "additionalProperties":false,
"properties":{
"tool":{"type":"string","enum":[batch.name]},
"arguments": arguments
}
});
definition
.input_schema
.as_object_mut()
.expect("object schema")
.remove("required");
definition.input_schema["oneOf"] = json!([{"required":["code"]},{"required":["batch"]}]);
}
if let Some(tool) = catalog
.iter_mut()
.find(|t| t.name == EXECUTE_TOOLS_TOOL_NAME)
{
tool.description = definition.description;
tool.input_schema = definition.input_schema;
}
}
pub(super) fn offered_batch(catalog: &[Tool]) -> Option<&Value> {
catalog
.iter()
.find(|tool| tool.name == EXECUTE_TOOLS_TOOL_NAME)?
.input_schema
.pointer("/properties/batch/properties")
}
pub(super) fn offered_batch_allows(catalog: &[Tool], name: &str, input: &Value) -> bool {
let Some(schema) = offered_batch(catalog)
.filter(|s| s.pointer("/tool/enum/0").and_then(Value::as_str) == Some(name))
else {
return false;
};
let Some((field, members)) = schema
.get("arguments")
.and_then(crate::mcp::named_batch_members)
else {
return false;
};
let bounds = &schema["arguments"]["properties"][field];
let min = bounds["minItems"].as_u64().unwrap_or(1).max(1);
let max = bounds["maxItems"].as_u64().unwrap_or(0);
input
.get(field)
.and_then(Value::as_array)
.is_some_and(|steps| {
(min..=max).contains(&(steps.len() as u64))
&& steps
.iter()
.all(|step| step.get("tool").is_some_and(|tool| members.contains(tool)))
})
}
pub fn initial_active_tools(catalog: &[Tool]) -> HashSet<String> {
let mut active = HashSet::new();
for tool in catalog {
@@ -845,7 +947,7 @@ fn unavailable_core_action_tools_with_regex(
Ok(cached_fallbacks()
.iter()
.filter(|cf| !catalog_contains_tool(catalog, cf.fallback.name))
.filter(|cf| regex.is_match(&cf.haystack))
.filter(|cf| regex.is_match(&cf.name_lower) || regex.is_match(&cf.haystack))
.take(max_results)
.map(|cf| cf.fallback)
.collect())
@@ -915,7 +1017,7 @@ fn discover_tools_with_regex(
continue;
}
scratch.load(tool);
if regex.is_match(&scratch.hay) {
if regex.is_match(&scratch.name_lower) || regex.is_match(&scratch.hay) {
matches.push(tool.name.clone());
}
if matches.len() >= max_results {
@@ -753,7 +753,7 @@ mod tests {
false,
),
(
json!({"tool": "consent", "arguments": {"action": "allow", "scope": "foreground"}}),
json!({"tool": "consent", "args": {"action": "allow", "scope": "foreground"}}),
false,
),
(
+27 -1
View File
@@ -808,7 +808,11 @@ pub(super) fn replace_runtime_mcp_tools(
}
tool_catalog.push(tool);
}
*tool_catalog != catalog_before || *active_tool_names != active_before
let changed = *tool_catalog != catalog_before || *active_tool_names != active_before;
if changed {
super::tool_catalog::configure_composition_entry(tool_catalog, None, &[]);
}
changed
}
/// Whether model-written Python may run on this turn at all: `code_execution`
@@ -1493,6 +1497,7 @@ impl Engine {
// registry-derived field as unknown rather than guessing.
inspection_surface: Option<crate::tool_inspection::ToolSurfaceContext>,
) -> (TurnOutcomeStatus, Option<String>) {
let _timing = codewhale_runtime::logging::PhaseTimer::start("turn_loop");
// R1: restart the cumulative per-turn wall-clock budget. This is the
// only place it is started, so exactly one turn owns it at a time.
self.turn_wall_clock =
@@ -1812,6 +1817,7 @@ impl Engine {
fleet_denial_guard: Option<&FleetDenialGuard>,
source: ToolCallSource,
) -> PlannedToolCalls {
let _timing = codewhale_runtime::logging::PhaseTimer::start("tool_planning");
// Definitions and services remain captured, while preparation reads
// the same current Engine posture as dispatch. A retained registry's
// spawn-time approval bit cannot override a later permission change.
@@ -3738,6 +3744,22 @@ impl Engine {
mode: AppMode,
extension: Option<crate::tools::codemode::ExtensionCaller>,
) -> (Result<RichToolResult, ToolError>, bool) {
let named_batch = tool_name == EXECUTE_TOOLS_TOOL_NAME
&& tool_input.get("batch").is_some_and(|v| !v.is_null());
if named_batch {
let batch = &tool_input["batch"];
let offered = super::tool_catalog::offered_batch(tool_catalog)
.and_then(|s| s.pointer("/tool/enum/0"))
.and_then(Value::as_str);
if offered.is_none() || batch.get("tool").and_then(Value::as_str) != offered {
return (
Err(ToolError::not_available(
"This named batch is not offered in this turn.",
)),
false,
);
}
}
let (gate, mut requests) = crate::tools::codemode::NestedCallGate::new(
mcp_pool.clone(),
self.tx_event.clone(),
@@ -3905,6 +3927,7 @@ impl Engine {
extension.as_ref(),
request.withdraw.as_ref(),
request.child_activation,
named_batch,
)
.await
};
@@ -4021,6 +4044,7 @@ impl Engine {
extension: Option<&crate::tools::codemode::ExtensionCaller>,
withdraw: Option<&tokio_util::sync::CancellationToken>,
child_activation: bool,
named_batch: bool,
) -> crate::tools::codemode::NestedCallVerdict {
use crate::tools::codemode::{NestedCallVerdict, NestedDecision};
let typed_child = |name: &str, input: &serde_json::Value| {
@@ -4133,6 +4157,8 @@ impl Engine {
.then(|| "a child activation may only start an independent agent".to_string())
.or_else(|| {
crate::tools::codemode::refusal_before_gate(&plan.name, &plan.input, true)
.or_else(|| (named_batch && !super::tool_catalog::offered_batch_allows(tool_catalog, &plan.name, &plan.input))
.then(|| "A batch step is not offered or the step count is outside the current batch bounds. Use its declared members and limits.".to_string()))
.or_else(|| refuse_early(tool_registry, &plan.name, &plan.input))
.filter(|_| !resolved_child)
})
@@ -11,6 +11,7 @@ impl Engine {
client: &SharedModelClient,
response: AcceptedModelStep,
) -> PhaseResult<AcceptedModelStep> {
let _timing = codewhale_runtime::logging::PhaseTimer::start("model_continuation");
if self.rlm_host.is_some() {
return self
.continue_rlm_model_step(turn, tool_policy, progress, client, response)
@@ -11,6 +11,7 @@ impl Engine {
client: &SharedModelClient,
prepared: PreparedModelStep,
) -> PhaseResult<AcceptedModelStep> {
let _timing = codewhale_runtime::logging::PhaseTimer::start("model_step");
let PreparedModelStep {
request_nudge,
prefix_check,
@@ -11,6 +11,7 @@ impl Engine {
client: &SharedModelClient,
inspection_surface: Option<&crate::tool_inspection::ToolSurfaceContext>,
) -> PhaseResult<PreparedModelStep> {
let _timing = codewhale_runtime::logging::PhaseTimer::start("model_preparation");
if self.rlm_host.is_some() {
return self
.prepare_rlm_model_step(turn, client, inspection_surface)
@@ -33,7 +34,7 @@ impl Engine {
&mut progress.active_tool_names,
)
.await;
self.record_mcp_runtime_context(tool_policy, &progress.tool_catalog)
self.record_mcp_runtime_context(tool_policy, &mut progress.tool_catalog)
.await;
self.record_current_constitution().await;
self.record_current_extension_prompt_contributions().await;
@@ -11,6 +11,7 @@ impl Engine {
client: &SharedModelClient,
response: AcceptedModelStep,
) -> PhaseResult<()> {
let _timing = codewhale_runtime::logging::PhaseTimer::start("tool_batch");
let tool_registry = Some(&tool_policy.registry);
let AcceptedModelStep {
current_text_visible,
@@ -130,6 +131,7 @@ impl Engine {
)
.await;
let _post_timing = codewhale_runtime::logging::PhaseTimer::start("tool_postprocessing");
let authority_changed = authority_changed_before_tools || authority_changed_during_tools;
self.turn_heartbeat.enter(
super::turn_heartbeat::TurnPhase::Preparing,
+159 -12
View File
@@ -1047,6 +1047,86 @@ pub struct McpTool {
pub annotations: Option<McpToolAnnotations>,
}
pub(crate) fn named_batch_members(
schema: &serde_json::Value,
) -> Option<(&str, &[serde_json::Value])> {
let required = schema.get("required")?.as_array()?;
let fields = schema.get("properties")?.as_object()?;
required
.iter()
.filter_map(serde_json::Value::as_str)
.find_map(|name| {
let steps = fields.get(name)?;
if steps.get("type")?.as_str()? != "array"
|| !(1..=50).contains(&steps.get("maxItems")?.as_u64()?)
{
return None;
}
let step = steps.get("items")?;
if !step
.get("required")?
.as_array()?
.iter()
.any(|key| key == "tool")
|| step.pointer("/properties/arguments/type")?.as_str()? != "object"
|| step.pointer("/properties/tool/type")?.as_str()? != "string"
{
return None;
}
let names = step.pointer("/properties/tool/enum")?.as_array()?;
(!names.is_empty() && names.len() <= 128 && names.iter().all(|v| v.is_string()))
.then_some((name, names.as_slice()))
})
}
fn batch_member_signature(tool: &McpTool) -> Option<String> {
fn identifier(value: &str) -> bool {
!value.is_empty()
&& value.len() <= 64
&& value
.bytes()
.all(|b| b.is_ascii_alphanumeric() || b == b'_' || b == b'-')
}
if !identifier(&tool.name) {
return None;
}
let fields = tool.input_schema.get("properties")?.as_object()?;
if fields.len() > 24 || !fields.keys().all(|key| identifier(key)) {
return None;
}
let required = tool.input_schema.get("required").and_then(|v| v.as_array());
let keys = fields
.iter()
.map(|(key, value)| {
let mut field = key.clone();
if required.is_none_or(|names| !names.iter().any(|name| name == key)) {
field.push('?');
}
if let Some(values) = value.get("enum").and_then(|v| v.as_array())
&& (1..=6).contains(&values.len())
&& values.iter().all(|v| v.as_str().is_some_and(identifier))
{
field.push('=');
field.push_str(
&values
.iter()
.filter_map(|v| v.as_str())
.collect::<Vec<_>>()
.join("|"),
);
}
if let (Some(min), Some(max)) = (
value.get("minimum").filter(|v| v.is_number()),
value.get("maximum").filter(|v| v.is_number()),
) {
field.push_str(&format!("={min}..{max}"));
}
field
})
.collect::<Vec<_>>();
Some(format!("{}({})", tool.name, keys.join(",")))
}
/// The subset of MCP `ToolAnnotations` the approval path reads.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Deserialize, Serialize)]
pub struct McpToolAnnotations {
@@ -4615,6 +4695,23 @@ impl McpPool {
visible_tools: &HashSet<String>,
discovery_available: bool,
failed: &HashSet<String>,
) -> Vec<(String, codewhale_mcp::ModelServerStatus)> {
self.model_server_statuses_for(
permitted,
visible_tools,
discovery_available,
failed,
&self.resolved_tool_servers(),
)
}
pub(crate) fn model_server_statuses_for(
&self,
permitted: impl Fn(&str) -> bool,
visible_tools: &HashSet<String>,
discovery_available: bool,
failed: &HashSet<String>,
routes: &std::collections::BTreeMap<String, String>,
) -> Vec<(String, codewhale_mcp::ModelServerStatus)> {
use codewhale_mcp::ModelServerStatus;
@@ -4626,16 +4723,16 @@ impl McpPool {
BTreeSet::new()
};
names.extend(
self.resolved_tool_servers()
.into_iter()
routes
.iter()
.filter(|(tool, server)| {
visible_tools.contains(tool)
visible_tools.contains(*tool)
&& permitted(server)
&& self
.server_config(server)
.is_some_and(|config| config.is_enabled())
})
.map(|(_, server)| server),
.map(|(_, server)| server.clone()),
);
let generation = self.current_catalog_generation();
let mut rows: Vec<_> = names
@@ -5208,13 +5305,23 @@ impl McpPool {
&self,
model_visible: impl Fn(&str) -> bool,
) -> Vec<(String, String)> {
let servers: BTreeSet<String> = self
.resolved_tool_servers()
.into_iter()
self.model_server_instructions_for(model_visible, None, &self.resolved_tool_servers())
.0
}
pub(crate) fn model_server_instructions_for(
&self,
model_visible: impl Fn(&str) -> bool,
composition: Option<&str>,
routes: &std::collections::BTreeMap<String, String>,
) -> (Vec<(String, String)>, Vec<String>) {
let mut allowed_members = Vec::new();
let servers: BTreeSet<String> = routes
.iter()
.filter(|(tool, _)| model_visible(tool))
.map(|(_, server)| server)
.map(|(_, server)| server.clone())
.collect();
servers
let instructions = servers
.into_iter()
.filter_map(|server| {
let conn = self.connections.get(&server)?;
@@ -5222,10 +5329,50 @@ impl McpPool {
{
return None;
}
let text = conn.instructions()?.to_string();
Some((server, text))
let guidance = conn.instructions().unwrap_or_default();
let batch = composition.filter(|name| {
routes.get(*name) == Some(&server) && model_visible(name)
});
let mut text = String::new();
if let Some((name, members)) = batch.and_then(|name| {
conn.tools().iter()
.find(|t| Self::mcp_model_tool_name(&server, &t.name) == name)
.and_then(|t| named_batch_members(&t.input_schema))
.map(|(_, members)| (name, members))
}) {
let available: Vec<_> = members.iter().filter_map(|member| {
let member = member.as_str()?;
let qualified = Self::mcp_model_tool_name(&server, member);
(routes.get(&qualified) == Some(&server) && model_visible(&qualified)
&& !crate::tools::approval_cache::computer_use_human_decision_tool(&qualified))
.then(|| conn.tools().iter().find(|t| t.name == member))?
}).collect();
if available.is_empty() {
return (!guidance.is_empty()).then(|| (server, guidance.to_string()));
}
text.push_str(&format!("Use execute_tools with batch={{tool:\"{name}\",arguments:{{...}}}} for known steps. Put the requested sequence and its postconditions together in one call when no new decision is needed. Use code to branch on intermediate results. Stop on refusal or uncertain delivery. "));
if conn.config().reviewed_plugin_source().is_some_and(|source| source.plugin_name() == "computer-use")
&& available.iter().any(|member| member.name == "open_application")
{
text.push_str("For a named Mac app, bind it first with open_application using the requested pid (or name/bundle_id) and activate:false. An app_ref read does not bind input. Named find selectors resolve a fresh target during the action, so known labels do not need a separate discovery turn. Use a selector per target field; any app_ref belongs inside each selector. Verify the requested effects in the same app; scope the final observe to its app_ref. ");
}
text.push_str("Member arguments (? optional):");
for tool in available {
allowed_members.push(tool.name.clone());
if let Some(signature) = batch_member_signature(tool)
&& text.len() + signature.len() < 2304
{
text.push(' ');
text.push_str(&signature);
}
}
text.push('\n');
}
text.push_str(guidance);
(!text.is_empty()).then_some((server, text))
})
.collect()
.collect();
(instructions, allowed_members)
}
/// Insert a ready, idle connection with the given tools and guidance, for
+19 -16
View File
@@ -18,11 +18,10 @@ use codewhale_core::ContextReference;
#[cfg(test)]
use codewhale_core::{ContextReferenceKind, ContextReferenceSource};
use codewhale_models::reasoning_effort_tier::ReasoningEffortTier;
use codewhale_models::turn_classify::same_recorded_turn;
use codewhale_models::{ContentBlock, Message, SystemPrompt};
use codewhale_runtime::artifacts::ArtifactRecord;
use codewhale_runtime::session_tree::{
SessionEntry, SessionImportContainer, SessionJournal, same_message_ignoring_turn_metadata,
};
use codewhale_runtime::session_tree::{SessionEntry, SessionImportContainer, SessionJournal};
use codewhale_util::utils::write_atomic;
use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, BTreeSet};
@@ -1232,7 +1231,7 @@ impl SavedSession {
/// `metadata.message_count` that disagrees with `messages.len()`. Rewriting
/// that count here would silently edit live data on every save.
pub fn make_storage_compatible(&mut self) {
let Some(journal) = self.journal.as_ref() else {
let Some(journal) = self.journal.as_mut() else {
return;
};
if self.messages.is_empty() {
@@ -1241,14 +1240,11 @@ impl SavedSession {
if self.messages == journal.to_messages() {
return;
}
// Split the `journal` / `messages` borrows; the take is returned
// before this function ends, so the session is never left short.
let messages = std::mem::take(&mut self.messages);
if let Some(journal) = self.journal.as_mut() {
journal.rebranch_active_messages(&messages);
self.leaf_id = journal.leaf_id.clone();
let report = journal.rebranch_active_messages(&self.messages);
self.leaf_id.clone_from(&journal.leaf_id);
if report.kept_recorded_metadata {
self.messages = journal.to_messages();
}
self.messages = messages;
}
self.metadata.message_count = self.messages.len();
}
@@ -1284,7 +1280,7 @@ impl SavedSession {
.messages
.iter()
.zip(&messages)
.take_while(|(old, new)| old == new || same_message_ignoring_turn_metadata(old, new))
.take_while(|(old, new)| same_recorded_turn(old, new))
.count();
self.turn_outcomes
.retain(|outcome| outcome.after_message_count <= unchanged);
@@ -5167,8 +5163,9 @@ pub fn update_session(
let common = messages
.iter()
.zip(session.messages.iter())
.take_while(|(a, b)| a == b || same_message_ignoring_turn_metadata(a, b))
.take_while(|(a, b)| same_recorded_turn(a, b))
.count();
let mut kept_recorded_metadata = false;
if new_len >= old_len && messages[..old_len] == session.messages[..] {
if let Some(journal) = session.journal.as_mut() {
for msg in &messages[old_len..] {
@@ -5179,11 +5176,17 @@ pub fn update_session(
} else if (new_len != old_len || messages != session.messages.as_slice())
&& let Some(journal) = session.journal.as_mut()
{
let _ = journal.rebranch_active_messages(messages);
kept_recorded_metadata = journal
.rebranch_active_messages(messages)
.kept_recorded_metadata;
session.leaf_id = journal.leaf_id.clone();
}
session.messages.clear();
session.messages.extend_from_slice(messages);
if kept_recorded_metadata && let Some(journal) = &session.journal {
session.messages = journal.to_messages();
} else {
session.messages.clear();
session.messages.extend_from_slice(messages);
}
session
.turn_outcomes
.retain(|outcome| outcome.after_message_count <= common);
+5 -2
View File
@@ -439,8 +439,11 @@ pub fn computer_use_batch_hidden_gate(tool_name: &str, input: &Value) -> Option<
.iter()
.find_map(|step| {
let inner = step.get("tool").and_then(Value::as_str)?;
let arguments = step.get("arguments").cloned().unwrap_or(Value::Null);
computer_use_user_gate(&format!("mcp_{inner}"), &arguments).map(|_| inner.to_string())
let arguments = step
.get("arguments")
.or_else(|| step.get("args"))
.unwrap_or(&Value::Null);
computer_use_user_gate(&format!("mcp_{inner}"), arguments).map(|_| inner.to_string())
})
}
+25 -8
View File
@@ -1468,7 +1468,29 @@ pub async fn execute_tools_tool(
registry: &ToolRegistry,
context: &ToolContext,
) -> Result<ToolResult, ToolError> {
let code = required_str(input, "code")?;
let (code, args) = match input.get("batch").filter(|value| !value.is_null()) {
Some(batch) => {
if input.get("code").is_some_and(|v| !v.is_null())
|| !batch.get("tool").is_some_and(Value::is_string)
|| !batch.get("arguments").is_some_and(Value::is_object)
|| batch.to_string().len() > MAX_CODE_BYTES
{
return Err(ToolError::invalid_input(
"Supply either code or a bounded batch with tool and arguments.",
));
}
if context.execution.nested_call_gate.is_none() {
return Err(ToolError::permission_denied(
"A named batch requires the session tool gate.",
));
}
(
"return await tools.call(args.tool, args.arguments);",
batch.clone(),
)
}
None => (required_str(input, "code")?, Value::Null),
};
if code.trim().is_empty() {
return Err(ToolError::missing_field("code"));
}
@@ -1482,13 +1504,8 @@ pub async fn execute_tools_tool(
let deadline = invoker.deadline();
let cancel = WorkflowRunCancel::new();
let vm = WorkflowVm::new();
let mut run = Box::pin(vm.run_tools_script(
code,
Value::Null,
driver.clone(),
invoker.clone(),
cancel.clone(),
));
let mut run =
Box::pin(vm.run_tools_script(code, args, driver.clone(), invoker.clone(), cancel.clone()));
let watchdog = async {
loop {
let Some(remaining) = invoker.remaining(deadline) else {
+2 -2
View File
@@ -22704,8 +22704,8 @@ const READ_ONLY_CHILD_ENVELOPE_BYTE_CEILING: usize = 89_112;
// restores the legacy Operate contract, less 30B from the background-shell
// schema edits, plus 112B for the display-only bash description. The
// context_archive_search and context_archive_get specs (#7173) add 751B. The
// ceiling keeps the 13B Linux margin.
const PARENT_SURFACE_BYTE_CEILING: usize = 91_556;
// ceiling keeps its existing platform margin after the reviewed schema growth.
const PARENT_SURFACE_BYTE_CEILING: usize = 91_812;
#[tokio::test]
async fn read_only_child_envelope_stays_within_measured_ceiling() {
+31 -1
View File
@@ -184,7 +184,37 @@ pub fn turn_metadata_text(message: &Message) -> Option<(usize, &str)> {
}
fn is_complete_turn_metadata(text: &str) -> bool {
text.starts_with("<turn_meta>") && text.ends_with("</turn_meta>")
text.strip_prefix("<turn_meta>")
.and_then(|body| body.split_once("</turn_meta>"))
.is_some_and(|(_, tail)| tail.is_empty())
}
pub fn same_recorded_turn(left: &Message, right: &Message) -> bool {
if left == right {
return true;
}
if left.role != Role::User || right.role != Role::User {
return false;
}
let (bare, stamped) = if left.content.len() < right.content.len() {
(left, right)
} else {
(right, left)
};
if stamped.content.len().checked_sub(1) != Some(bare.content.len()) {
return false;
}
let Some((index, metadata)) = turn_metadata_text(stamped) else {
return false;
};
if !metadata.starts_with("<turn_meta>\nCurrent local date: ")
|| classify_user_turn_prompt(stamped) == UserTurnPromptKind::NotPrompt
|| turn_metadata_text(bare).is_some()
{
return false;
}
stamped.content[..index] == bare.content[..index]
&& stamped.content[index + 1..] == bare.content[index..]
}
/// Recognize both the current condensed provenance line and the legacy
+7 -5
View File
@@ -491,18 +491,20 @@ pub fn is_codewhale_binary(path: &Path) -> bool {
}
pub fn file_sha256(path: &Path) -> Result<String> {
let mut file =
let file =
fs::File::open(path).with_context(|| format!("failed to open {}", path.display()))?;
reader_sha256(file).with_context(|| format!("failed to read {}", path.display()))
}
pub fn reader_sha256(mut reader: impl Read) -> std::io::Result<String> {
let mut hasher = Sha256::new();
let mut buffer = vec![0u8; 1 << 20];
loop {
let read = match file.read(&mut buffer) {
let read = match reader.read(&mut buffer) {
Ok(0) => break,
Ok(read) => read,
Err(error) if error.kind() == std::io::ErrorKind::Interrupted => continue,
Err(error) => {
return Err(error).with_context(|| format!("failed to read {}", path.display()));
}
Err(error) => return Err(error),
};
hasher.update(&buffer[..read]);
}
+34
View File
@@ -9,6 +9,40 @@ static VERBOSE: AtomicBool = AtomicBool::new(false);
#[cfg(windows)]
static VERBOSE_SNAPSHOT: AtomicBool = AtomicBool::new(false);
pub struct PhaseTimer {
phase: &'static str,
started: std::time::Instant,
}
impl PhaseTimer {
pub fn start(phase: &'static str) -> Option<Self> {
static ENABLED: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
ENABLED
.get_or_init(|| std::env::var("CODEWHALE_TIMING").is_ok_and(|value| value == "1"))
.then(|| Self {
phase,
started: std::time::Instant::now(),
})
}
}
impl Drop for PhaseTimer {
fn drop(&mut self) {
use std::io::Write;
let receipt = serde_json::json!({
"type": "execution_timing",
"phase": self.phase,
"pid": std::process::id(),
"elapsed_us": self.started.elapsed().as_micros(),
"ended_unix_us": std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.ok()
.map(|elapsed| elapsed.as_micros()),
});
let _ = writeln!(std::io::stderr().lock(), "{receipt}");
}
}
/// Enable or disable verbose logging output.
pub fn set_verbose(enabled: bool) {
VERBOSE.store(enabled, Ordering::SeqCst);
+60 -119
View File
@@ -43,61 +43,6 @@ pub enum SessionEntryKind {
content: String,
},
}
/// `a` and `b` carry the same content. Engine-owned `<turn_meta>` metadata is
/// masked, so a transcript that gained or dropped the envelope still matches
/// the turn it already stored, and a role change never does.
pub fn same_message_ignoring_turn_metadata(a: &Message, b: &Message) -> bool {
a.role == b.role && normalized_blocks(a).eq(normalized_blocks(b))
}
/// A block that is nothing but one `<turn_meta>…</turn_meta>` envelope.
/// [`strip_leading_turn_metadata`] decides it, so a block that merely starts
/// and ends with the tags — `<turn_meta>x</turn_meta>real text</turn_meta>` —
/// keeps its trailing text and is not treated as metadata-only.
fn is_complete_turn_metadata_block(text: &str) -> bool {
text.trim_start().starts_with("<turn_meta>") && strip_leading_turn_metadata(text).is_empty()
}
fn strip_leading_turn_metadata(text: &str) -> &str {
let trimmed = text.trim_start();
let Some(rest) = trimmed.strip_prefix("<turn_meta>") else {
return text;
};
let Some(end) = rest.find("</turn_meta>") else {
return text;
};
rest[end + "</turn_meta>".len()..].trim_start()
}
/// Borrowed comparison form: text blocks compare as their stripped slice, so
/// the relaxed comparison clones nothing and cannot drop a payload. The view
/// is an iterator over the message's own blocks, so it allocates nothing
/// either. Only `Text` is masked, so every other variant compares verbatim.
#[derive(PartialEq)]
enum NormalizedBlock<'a> {
Text(&'a str),
Other(&'a ContentBlock),
}
fn normalized_blocks(message: &Message) -> impl Iterator<Item = NormalizedBlock<'_>> {
let blocks = &message.content;
let mut start = 0;
let mut end = blocks.len();
if end - start >= 2
&& matches!(&blocks[start], ContentBlock::Text { text, .. } if is_complete_turn_metadata_block(text))
{
start += 1;
}
if end - start >= 2
&& matches!(&blocks[end - 1], ContentBlock::Text { text, .. } if is_complete_turn_metadata_block(text))
{
end -= 1;
}
blocks[start..end].iter().map(|block| match block {
ContentBlock::Text { text, .. } => NormalizedBlock::Text(strip_leading_turn_metadata(text)),
other => NormalizedBlock::Other(other),
})
}
impl SessionEntryKind {
pub fn is_contextual(&self) -> bool {
matches!(
@@ -105,27 +50,12 @@ impl SessionEntryKind {
Self::Message { .. } | Self::User { .. } | Self::Assistant { .. }
)
}
/// Exactly the stored message, without synthesizing legacy variants.
pub fn exact_message(&self) -> Option<&Message> {
match self {
Self::Message { message } => Some(message),
_ => None,
}
}
/// `self.as_message()` equals `message`, byte-for-byte. Only `Message`
/// entries also match after removing engine-owned `<turn_meta>` metadata
/// from both sides: that envelope is written by the engine into stored
/// messages, while `Compaction` and `BranchSummary` project to text this
/// crate synthesizes, so the live transcript must spell it exactly. A
/// kind that can only match relaxed would be selected for a rewrite it
/// cannot receive.
pub fn projects_to(&self, message: &Message) -> bool {
match self {
Self::Message { message: own } => {
own == message || same_message_ignoring_turn_metadata(own, message)
codewhale_models::turn_classify::same_recorded_turn(own, message)
}
other => other.as_message().is_some_and(|own| &own == message),
other => other.as_message().as_ref() == Some(message),
}
}
pub fn as_message(&self) -> Option<Message> {
@@ -199,6 +129,7 @@ impl SessionEntry {
pub struct Rebranch {
pub previous_active: usize,
pub shared_prefix: usize,
pub kept_recorded_metadata: bool,
}
impl Rebranch {
@@ -412,15 +343,8 @@ impl SessionJournal {
self.active_messages(true)
}
/// Make `messages` the active projection without rewriting the journal.
///
/// A prefix that differs only by engine `<turn_meta>` metadata is refreshed
/// in place, so the stored shape follows the live transcript instead of
/// forking a second copy of the same turn. That rewrite keeps the entry id,
/// `created_at` and parent link — ordering comes from the parent chain — and
/// replaces the previously stored payload, so the old shape is not kept as
/// a sibling. Anything past the prefix is appended as a sibling branch and
/// stays reachable.
/// Keep the shared prefix and its original stamps. A provisional user
/// entry can gain its Engine stamp until another branch shares it.
pub fn rebranch_active_messages(&mut self, messages: &[Message]) -> Rebranch {
self.rebranch_active_messages_stamped(messages, &[])
}
@@ -432,40 +356,55 @@ impl SessionJournal {
messages: &[Message],
stamps: &[DateTime<Utc>],
) -> Rebranch {
let (previous_active, shared_prefix, refreshed, leaf_id) = {
let active_path = self.root_to_leaf();
let previous_active = active_path.len();
let shared_prefix = active_path
.iter()
.zip(messages)
.take_while(|(entry, message)| entry.kind.projects_to(message))
.count();
let refreshed: Vec<(EntryId, Message)> = (0..shared_prefix)
.filter(|&index| {
matches!(
active_path[index].kind.exact_message(),
Some(stored) if stored != &messages[index]
)
})
.map(|index| (active_path[index].id.clone(), messages[index].clone()))
.collect();
let leaf_id = shared_prefix
.checked_sub(1)
.map(|index| active_path[index].id.clone());
(previous_active, shared_prefix, refreshed, leaf_id)
};
self.leaf_id = leaf_id;
for (id, message) in refreshed {
let Some(entry) = self.entries.iter_mut().find(|entry| entry.id == id) else {
tracing::warn!(
entry_id = %id,
"rebranch: refreshed entry vanished before the rewrite; the \
stored prefix keeps its previous shape"
);
continue;
};
if let SessionEntryKind::Message { message: stored } = Arc::make_mut(&mut entry.kind) {
*stored = message;
let path = self.root_to_leaf();
let previous_active = path.len();
let mut shared_prefix: usize = 0;
let mut kept_recorded_metadata = false;
let mut refresh = HashMap::new();
let mut protected_prefix = None;
for (index, (entry, incoming)) in path.iter().zip(messages).enumerate() {
if !entry.kind.projects_to(incoming) {
break;
}
if let SessionEntryKind::Message { message: stored } = &*entry.kind
&& stored != incoming
{
if stored.content.len() > incoming.content.len() {
kept_recorded_metadata = true;
} else {
let shared = protected_prefix.get_or_insert_with(|| {
if path.len() == self.entries.len() {
return 0;
}
let mut parents = HashSet::new();
let fork_points: HashSet<_> = self
.entries
.iter()
.filter_map(|entry| entry.parent_id.as_deref())
.filter(|parent| !parents.insert(*parent))
.collect();
path.iter()
.rposition(|entry| fork_points.contains(entry.id.as_str()))
.map_or(0, |index| index + 1)
});
if index < *shared {
break;
}
refresh.insert(entry.id.clone(), incoming);
}
}
shared_prefix += 1;
}
self.leaf_id = shared_prefix
.checked_sub(1)
.map(|index| path[index].id.clone());
if !refresh.is_empty() {
for entry in &mut self.entries {
if let Some(&incoming) = refresh.get(&entry.id) {
entry.kind = Arc::new(SessionEntryKind::Message {
message: incoming.clone(),
});
}
}
}
for (index, message) in messages.iter().enumerate().skip(shared_prefix) {
@@ -479,6 +418,7 @@ impl SessionJournal {
Rebranch {
previous_active,
shared_prefix,
kept_recorded_metadata,
}
}
@@ -1270,9 +1210,10 @@ mod tests {
let before = j.entries.len();
let mut stamped = msg("user", "hi");
if let ContentBlock::Text { text, .. } = &mut stamped.content[0] {
*text = format!("<turn_meta>{{\"turn\":1}}</turn_meta>\n{text}");
}
stamped.content.push(ContentBlock::Text {
text: "<turn_meta>\nCurrent local date: 2026-10-31\nCurrent workspace: /fixture\n</turn_meta>".into(),
cache_control: None,
});
let live = vec![stamped, msg("assistant", "there")];
let report = j.rebranch_active_messages(&live);
@@ -1329,7 +1270,7 @@ mod tests {
assert!(
j.entries
.iter()
.any(|entry| entry.kind.exact_message() == Some(&stored)),
.any(|entry| matches!(&*entry.kind, SessionEntryKind::Message { message } if message == &stored)),
"the stored payload stays in the journal as its own branch"
);
}
+7 -4
View File
@@ -67,7 +67,7 @@ fn rendered_catalogue(registry: &SkillRegistry, locale: &str, workspace: &Path)
render_skills_block(registry, locale, workspace).expect("non-empty registry renders a block")
}
/// Canonical names that appear as `- <name>: …` entries under
/// Canonical names with or without descriptions that appear under
/// `### Available skills` (and only there — the warnings section also uses
/// `- ` bullets).
fn catalogue_entry_names(block: &str) -> Vec<String> {
@@ -82,9 +82,12 @@ fn catalogue_entry_names(block: &str) -> Vec<String> {
continue;
}
if let Some(rest) = line.strip_prefix("- ")
&& let Some((name, _)) = rest.split_once(':')
&& !rest.starts_with("...")
{
names.push(name.trim().to_string());
let name = rest.split_once(':').map_or(rest, |(name, _)| name);
if !name.contains(char::is_whitespace) {
names.push(name.to_string());
}
}
}
names
@@ -227,7 +230,7 @@ fn explicit_only_skills_are_absent_from_the_ambient_catalogue() {
.filter(|e| !e.in_model_catalogue)
{
assert!(
!block.contains(&format!("- {}: ", entry.name)),
!catalogue_entry_names(&block).contains(&entry.name),
"{} is explicit-only and must not become ambient context",
entry.name
);
+6 -5
View File
@@ -45,7 +45,8 @@ use codewhale_runtime::{
/// — survives shortening. When the index shortens any row it ends with a
/// count footer that `/context report` repeats, and `/skills inspect` notes each
/// description over this cap.
pub const DEFAULT_SKILL_DESCRIPTION_CHARS: usize = 400;
pub const DEFAULT_SKILL_DESCRIPTION_CHARS: usize = 120;
const MAX_SKILL_WARNING_CHARS: usize = 400;
/// Floor for the model-facing skill index budget, in chars. The real budget
/// scales with the route's context window ([`SkillListingConfig::budget_chars`]);
/// this floor keeps tiny local windows from erasing the index altogether.
@@ -55,7 +56,7 @@ const MIN_AVAILABLE_SKILLS_CHARS: usize = 2_400;
const MAX_AVAILABLE_SKILLS_CHARS_CEILING: usize = 40_000;
/// Share of the context window the ambient index may take. Conservative on
/// purpose — the index is routing metadata, not the work.
const SKILL_BUDGET_CONTEXT_PERCENT: u64 = 5;
const SKILL_BUDGET_CONTEXT_PERCENT: f64 = 0.2;
/// Chars-per-token estimate for the budget; matches the conservative
/// estimator used by the context report.
const SKILL_BUDGET_CHARS_PER_TOKEN: u64 = 4;
@@ -90,7 +91,7 @@ impl Default for SkillListingConfig {
impl SkillListingConfig {
const DEFAULT: Self = Self {
max_description_chars: DEFAULT_SKILL_DESCRIPTION_CHARS,
budget_fraction: SKILL_BUDGET_CONTEXT_PERCENT as f64 / 100.0,
budget_fraction: SKILL_BUDGET_CONTEXT_PERCENT / 100.0,
};
pub fn from_config(config: &codewhale_runtime::config::SkillsConfig) -> Self {
@@ -1992,7 +1993,7 @@ Skills are optional instruction packs. This index exposes routing metadata; bodi
let warning_slice = if warnings.is_empty() {
0
} else {
(budget_chars / 5).min(8 * (DEFAULT_SKILL_DESCRIPTION_CHARS + 4))
(budget_chars / 5).min(8 * (MAX_SKILL_WARNING_CHARS + 4))
};
let index_budget = budget_chars.saturating_sub(fixed + warning_slice);
let shortened_budget = index_budget.saturating_sub(shortened_reserve);
@@ -2112,7 +2113,7 @@ Skills are optional instruction packs. This index exposes routing metadata; bodi
"- {}\n",
truncate_for_prompt(
&sanitize_prompt_path_text(warning, workspace, configured_skills_root),
DEFAULT_SKILL_DESCRIPTION_CHARS,
MAX_SKILL_WARNING_CHARS,
)
);
let len = line.chars().count();
+20 -8
View File
@@ -635,7 +635,7 @@ fn render_available_skills_context_collapses_internal_whitespace() {
fn render_available_skills_context_keeps_every_name_when_descriptions_overflow() {
let tmpdir = TempDir::new().unwrap();
let big_desc = "y".repeat(super::DEFAULT_SKILL_DESCRIPTION_CHARS - 20);
for i in 0..200 {
for i in 0..100 {
let body = format!("---\nname: skill-{i:03}\ndescription: {big_desc}\n---\nbody");
create_skill_dir(&tmpdir, &format!("skill-{i:03}"), &body);
}
@@ -643,9 +643,9 @@ fn render_available_skills_context_keeps_every_name_when_descriptions_overflow()
let rendered = crate::render_available_skills_context(&tmpdir.path().join("skills"))
.expect("skill context");
// 200 × ~380 chars of description is ~76k, far over the default budget:
// 100 × 100 chars of description is ~10k, far over the default budget:
// tier 1 cannot fit, so descriptions shrink or names stand alone.
for i in 0..200 {
for i in 0..100 {
let name = format!("- skill-{i:03}");
assert!(rendered.contains(&name), "missing {name}:\n{rendered}");
}
@@ -684,8 +684,20 @@ fn render_skills_block_shortens_summary_before_trigger() {
source: super::SkillSource::Native,
});
}
let rendered =
super::render_skills_block(&registry, "en", tmpdir.path()).expect("skill context");
let budget = 25_600;
let config = super::SkillListingConfig {
max_description_chars: 400,
..Default::default()
};
let rendered = super::render_skills_block_with_configured_root(
&registry,
"en",
tmpdir.path(),
None,
config,
budget,
)
.expect("skill context");
let line = rendered
.lines()
.find(|l| l.starts_with("- skill-007:"))
@@ -698,7 +710,7 @@ fn render_skills_block_shortens_summary_before_trigger() {
!line.contains(&summary),
"summary must be the half that shrinks:\n{line}"
);
assert!(rendered.chars().count() <= super::MAX_AVAILABLE_SKILLS_CHARS);
assert!(rendered.chars().count() <= budget);
}
/// The budget follows the route window: a 1M route sees a much larger index
@@ -709,8 +721,8 @@ fn skills_prompt_budget_scales_with_context_window() {
let default = super::SkillListingConfig::default().budget_chars(None);
let large = super::SkillListingConfig::default().budget_chars(Some(1_000_000));
assert_eq!(small, 2_400, "floor holds for tiny windows");
assert_eq!(default, 25_600, "128k window × 4 chars × 5%");
assert_eq!(large, 40_000, "ceiling holds for 1M windows");
assert_eq!(default, 2_400, "128k window × 4 chars × 0.2%, floored");
assert_eq!(large, 8_000, "1M window × 4 chars × 0.2%");
let configured =
super::SkillListingConfig::from_config(&codewhale_runtime::config::SkillsConfig {
listing_budget_fraction: Some(0.01),
+139 -2
View File
@@ -1769,8 +1769,12 @@ fn parse_inline_spans(line: &str, base_style: Style, link_style: Style) -> Vec<I
&& let Some(ch) = escaped.chars().next()
&& ch.is_ascii_punctuation()
{
out.push(InlineToken::new(ch.to_string(), base_style, None));
rest = &escaped[ch.len_utf8()..];
// Carry any combining marks that extend the escaped character into
// one grapheme cluster: `\*️⃣` escapes the `*` of a keycap, and
// emitting that `*` alone left the two marks in a span of their own.
let cluster = escaped.graphemes(true).next().unwrap_or_default();
out.push(InlineToken::new(cluster.to_string(), base_style, None));
rest = &escaped[cluster.len()..];
continue;
}
// **bold**
@@ -1792,6 +1796,10 @@ fn parse_inline_spans(line: &str, base_style: Style, link_style: Style) -> Vec<I
// *italic*
if rest.starts_with('*')
&& !rest.starts_with("**")
// The first grapheme has to be a lone `*`. When it is the base of a
// cluster (`*️⃣` is U+002A U+FE0F U+20E3) it is not a delimiter, so
// treating it as one ate the `*` and split the marks off.
&& rest.graphemes(true).next() == Some("*")
&& let Some(end) = rest[1..].find('*')
{
let inner = &rest[1..1 + end];
@@ -1811,6 +1819,7 @@ fn parse_inline_spans(line: &str, base_style: Style, link_style: Style) -> Vec<I
// _italic_
if rest.starts_with('_')
&& !rest.starts_with("__")
&& rest.graphemes(true).next() == Some("_")
&& let Some(end) = rest[1..].find('_')
{
let inner = &rest[1..1 + end];
@@ -1901,6 +1910,22 @@ fn parse_inline_spans(line: &str, base_style: Style, link_style: Style) -> Vec<I
}
// Plain text: consume until next marker or URL; always advance at least 1 char.
let next = find_next_marker(rest).max(rest.chars().next().map_or(1, |c| c.len_utf8()));
// Never split a grapheme cluster. A marker character can be the base of
// one: the `*` keycap is U+002A U+FE0F U+20E3, a single cluster, and
// `*` alone is not emphasis here (no closing `*`), so the marker search
// stops right after it. Cutting there left the two zero-width marks in
// a span of their own; the terminal then drew the cluster two columns
// wide while the spans reported one, and every separator after the cell
// moved off the box. A digit keycap never hit this because a digit is
// not a marker, which is why only `*` broke.
let next = rest
.grapheme_indices(true)
.map(|(start, grapheme)| start + grapheme.len())
.find(|end| *end >= next)
// `grapheme_indices` ends at `rest.len()`, so the find always
// succeeds. Clamp rather than reuse `next` so a future change to
// `find_next_marker` cannot slice out of bounds here.
.unwrap_or(rest.len());
out.push(InlineToken::new(rest[..next].to_string(), base_style, None));
rest = &rest[next..];
}
@@ -2344,6 +2369,118 @@ mod tests {
.collect()
}
/// Render `lines` into a fresh ratatui buffer at the given size.
fn buffer_of(lines: &[Line<'static>], width: u16, height: u16) -> ratatui::buffer::Buffer {
use ratatui::widgets::{Paragraph, Widget};
let area = ratatui::layout::Rect::new(0, 0, width, height);
let mut buf = ratatui::buffer::Buffer::empty(area);
Paragraph::new(lines.to_vec()).render(area, &mut buf);
buf
}
/// True column indices in `buf` whose symbol is the `|` box-drawing
/// separator. Scanning columns (not string chars) matters: a wide grapheme
/// such as a keycap fills one cell and leaves the next empty, so a char
/// index over a flattened row would not be a column index.
fn border_columns(buf: &ratatui::buffer::Buffer, width: u16, y: u16) -> Vec<usize> {
(0..width)
.filter(|x| buf[(*x, y)].symbol() == "\u{2502}")
.map(|x| x as usize)
.collect()
}
/// Flatten one buffer row for assertion messages.
fn row_text(buf: &ratatui::buffer::Buffer, width: u16, y: u16) -> String {
(0..width).map(|x| buf[(x, y)].symbol()).collect()
}
#[test]
fn keycap_table_puts_every_border_in_the_same_column() {
// A cell that holds a keycap must not shift the separators after it.
// `Line::width()` cannot see the drift because it sums span widths,
// so compare the columns the buffer actually filled.
let md = "| A | B |\n| --- | --- |\n\
| 1\u{fe0f}\u{20e3} | x |\n| *\u{fe0f}\u{20e3} | y |\n\
| #\u{fe0f}\u{20e3} | z |\n| plain | w |";
let lines = render_markdown(md, 80, Style::default());
let height = lines.len() as u16;
let buf = buffer_of(&lines, 80, height);
// Top and bottom borders use corner and tee glyphs, so compare only the
// rows that carry a `|` separator against each other.
let separated: Vec<(u16, Vec<usize>)> = (0..height)
.map(|y| (y, border_columns(&buf, 80, y)))
.filter(|(_, cols)| !cols.is_empty())
.collect();
assert!(
separated.len() >= 3,
"expected a header and data rows with separators"
);
let expected = separated[0].1.clone();
for (y, cols) in &separated {
assert_eq!(
cols,
&expected,
"row {y} separators drifted: {:?}",
row_text(&buf, 80, *y)
);
}
// The separators alone cannot tell the two shapes apart: both the split
// form (`*` plus a zero-width mark span) and the joined form advance the
// buffer by the same number of columns, so assert the symbol too. Before
// the fix this cell held a bare `*` and the keycap never reached the
// terminal at all.
let row_with_keycap = (0..height)
.map(|y| (y, row_text(&buf, 80, y)))
.find(|(_, text)| text.contains('*'))
.map(|(y, _)| y);
if let Some(y) = row_with_keycap {
let symbols: Vec<&str> = (0..80).map(|x| buf[(x, y)].symbol()).collect();
assert!(
symbols.iter().any(|s| s.contains("\u{20e3}")),
"row {y} lost the keycap: the cell holds {:?} instead of the \
whole cluster, so the terminal would draw a bare `*`",
symbols
.iter()
.filter(|s| !s.trim().is_empty())
.collect::<Vec<_>>()
);
}
}
#[test]
fn escaped_star_keycap_stays_in_one_token() {
// `\*️⃣` escapes the `*` of a keycap. Emitting that `*` on its own left
// the two combining marks in a second span, which is the same split the
// plain-text path had.
let tokens = parse_inline_spans("\\*\u{fe0f}\u{20e3}", Style::default(), Style::default());
assert_eq!(
tokens.len(),
1,
"escaped keycap split into {:?}",
tokens.iter().map(|t| t.text.as_str()).collect::<Vec<_>>()
);
assert_eq!(tokens[0].text, "*\u{fe0f}\u{20e3}");
}
#[test]
fn a_star_keycap_does_not_open_emphasis() {
// `*️⃣abc*` must not italicise `abc`: the leading `*` belongs to the
// keycap, so it cannot be an emphasis delimiter.
let tokens =
parse_inline_spans("*\u{fe0f}\u{20e3}abc*", Style::default(), Style::default());
assert!(
tokens
.iter()
.all(|t| !t.style.add_modifier.contains(Modifier::ITALIC)),
"keycap `*` opened emphasis: {:?}",
tokens.iter().map(|t| t.text.as_str()).collect::<Vec<_>>()
);
assert_eq!(tokens[0].text, "*\u{fe0f}\u{20e3}");
}
#[test]
fn backslash_escapes_yield_the_literal_punctuation() {
let lines = render_markdown(
+8 -2
View File
@@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- **Composition on the first request:** `execute_tools` accepts a bounded named batch from the current MCP catalog, with argument names from its allowed members and the same nested gate as a JavaScript program. Exact tool-name searches work with regex anchors. Stream receipts hash the executable in the background; `CODEWHALE_TIMING=1` reports phase durations. (CW-7649)
- **The model can read what compaction replaced (#7173):** two read-only tools,
`context_archive_search` and `context_archive_get`, search the session's
saved pre-compaction snapshots and read one archived message by id, with
@@ -28,7 +29,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **Runcrate is a bundled provider:** `runcrate` reaches the OpenAI-compatible host at `https://api.runcrate.ai/v1` with `RUNCRATE_API_KEY`, and model ids come from its public `GET /v1/models` listing. Thanks @heydryft for the request and the vendor details. (#7085)
- **MCP connection status reaches the model:** one existing history snapshot now records permitted configured, connecting, failed, authorization-required, unknown and connected servers, with bounded inert names and no raw errors. Stale status and server guidance are withdrawn on contention, configuration/scope changes and restore; observation does not connect servers or promise callable tools. (#6866)
- **Bundled-skill policy:** `[skills.bundled]` separates automatic, authoritative and disabled installation from discovery, with per-name selection, disable and explicit-only controls. Aliases share policy; disabled shipped copies reveal user alternatives. Modified and managed files remain intact, and doctor plus `/skills inspect` show the effective policy. (#7066)
- **Configurable skill routing index:** `[skills] listing_max_description_chars` and `listing_budget_fraction` control the shared parent/child catalogue. Defaults stay 400 characters and 5% of the route window, within the existing total bounds. Load warnings, localized inspect notes and `/context` explain shortening; full skill loading stays available. (#7073)
- **Configurable skill routing index:** `[skills] listing_max_description_chars` and `listing_budget_fraction` control the shared parent/child catalogue. Defaults use 120 characters and 0.2% of the route window, within the existing total bounds. Load warnings, localized inspect notes and `/context` explain shortening; full skill loading stays available. (#7073)
- **Compact approvals keep their authority visible:** cards show three choices and default to Deny. An exact saved rule requires its scope preview on screen. Shortcuts for additional actions require opening Details; a hidden saved-rule choice never falls back to granting the session.
- **Quicker everyday navigation:** top-level help separates everyday and advanced commands, `doctor --all` expands path/backend details, and the launch screen labels how to resume recent work. Up selects the newest recent session. Whale cameos stay out of transcript text.
@@ -157,6 +158,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
- Saving a user turn when the Engine adds its metadata preserves the journal entry and its place in history. Sessions with a fork keep the earlier branch intact. Thanks [@SparkofSpike](https://github.com/SparkofSpike). ([#7180](https://github.com/codewhale-hq/Codewhale/pull/7180))
- **Star keycaps stay intact in Markdown (#7176):** plain and escaped text
preserve the full grapheme, and a keycap cannot open emphasis. The separate
Windows Terminal font-fallback issue remains unqualified. Thanks
[@SparkofSpike](https://github.com/SparkofSpike).
- **Codewhale runs from a HarmonyOS user directory (#7168):** on HarmonyOS hmdfs (the 2in1 `/storage/Users/currentUser` mount), which reports its own owner and a group-writable mode for every object and refuses `chmod` and advisory locks, a GUI start failed one feature at a time — the workspace lease, the owner control socket, then the OAuth credential directory. Private directories, the secret store and credential files now probe the mount once, keep the strict ownership check everywhere it can be expressed, and on a mount that cannot express ownership still enforce no-follow, single-link and atomic publication, with one warning naming what the mount reported. A filesystem without advisory locks serialises secret-store writes within the process instead of refusing; a held lock is still contention. Not observed here on a real HarmonyOS device. Thanks [@gaord](https://github.com/gaord).
- **Notes stop swallowing a backslash:** a transcript note is machine-generated and names paths, but it went through the markdown renderer, where CommonMark §2.4 reads a backslash before ASCII punctuation as an escape. A note naming `C:\Users\asus\.codewhale\setup_state.json` therefore printed as `C:\Users\asus.codewhale\setup_state.json` — a directory that does not exist, in the one line a user would copy to find the real one. Notes are now escaped where they are rendered. Fenced blocks, and lines carrying inline code or a table row, are passed through untouched, because the renderer already copies those verbatim and doubling a backslash inside one would print it doubled.
- **Setup state survives a second instance:** two running Codewhale windows share one home, and both rewrite `setup_state.json` when the provider or model changes. The replacing rename is now retried briefly when the destination is held open for a read — Windows refuses it outright while that handle lives — instead of surfacing a write the caller could have made. The failure message also carries the underlying I/O error instead of only the outer context, so the next occurrence is diagnosable.
@@ -338,7 +344,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **[@gaord](https://github.com/gaord)** — a running turn's cost and context in the Runtime ([#7074](https://github.com/codewhale-hq/Codewhale/pull/7074)) forks that measure the history they carry ([#7147](https://github.com/codewhale-hq/Codewhale/pull/7147)), and private state on a HarmonyOS mount that cannot express ownership ([#7168](https://github.com/codewhale-hq/Codewhale/pull/7168)).
- **[@luochen211](https://github.com/luochen211)** — one form per term in the Simplified Chinese pack ([#7150](https://github.com/codewhale-hq/Codewhale/pull/7150)) and dropped blanket dead-code allows in fleet alerts and the pet worker ([#7149](https://github.com/codewhale-hq/Codewhale/pull/7149)).
- **[@gannp](https://github.com/gannp)** — reported vLLM effort values that ignored the local model template and fixed it with a reviewed template registry ([#7124](https://github.com/codewhale-hq/Codewhale/issues/7124), [#7125](https://github.com/codewhale-hq/Codewhale/pull/7125)).
- **[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)) and made a goal pause to ask instead of inventing the answer ([#7175](https://github.com/codewhale-hq/Codewhale/pull/7175)), and gave the model a way back into the transcript compaction replaced ([#7173](https://github.com/codewhale-hq/Codewhale/pull/7173)).
- **[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)) and made a goal pause to ask instead of inventing the answer ([#7175](https://github.com/codewhale-hq/Codewhale/pull/7175)), and gave the model a way back into the transcript compaction replaced ([#7173](https://github.com/codewhale-hq/Codewhale/pull/7173)). Also preserved journal history when a provisional input receives its Engine metadata ([#7180](https://github.com/codewhale-hq/Codewhale/pull/7180)).
- **[@ddlw](https://github.com/ddlw)** — repaired main for the 0.10.3 batch: clippy `-D warnings` across protocol, runtime, util and tui (boxed review and MCP-add payloads, scoped dead-code expectations, cfg-gated platform test helpers), rustfmt, split extension-host imports, the provider-registry identity check, Windows-stable DSH receipt bytes, and the first-party marketplace receipt ([#7108](https://github.com/codewhale-hq/Codewhale/pull/7108), [#7109](https://github.com/codewhale-hq/Codewhale/pull/7109)).
- **[@laozidabo](https://github.com/laozidabo)** — localized `/diff` feedback ([#7101](https://github.com/codewhale-hq/Codewhale/pull/7101)), flushed queued saves before signal exit ([#7122](https://github.com/codewhale-hq/Codewhale/pull/7122)) and kept `/rc` start failures visible ([#7127](https://github.com/codewhale-hq/Codewhale/pull/7127)).
- **[Boyce / @Aboysky](https://github.com/Aboysky)** — refreshed the README translation source stamps ([#7104](https://github.com/codewhale-hq/Codewhale/pull/7104)) and localized `/undo` feedback ([#7121](https://github.com/codewhale-hq/Codewhale/pull/7121)).
+11 -2
View File
@@ -220,6 +220,8 @@ pub(crate) async fn run_exec_agent(
use codewhale_engine::tools::todo::new_shared_todo_list;
use codewhale_execpolicy::ApprovalMode;
let _timing = codewhale_runtime::logging::PhaseTimer::start("exec_total");
let startup_timing = codewhale_runtime::logging::PhaseTimer::start("exec_startup");
ignore_sigpipe_for_headless_exec();
// Withhold `request_user_input`; a headless run has no responder.
@@ -343,7 +345,8 @@ pub(crate) async fn run_exec_agent(
"sha256:{}",
codewhale_runtime::hashing::sha256_hex(prompt.as_bytes())
);
let binary_sha256 = current_binary_sha256();
let mut binary_identity =
(output_format == ExecOutputFormat::StreamJson).then(start_binary_sha256);
let approval_posture = if auto_approve { "auto_tools" } else { "ask" }.to_string();
// The Full Access posture travels in the op's auto_approve/approval_mode
// fields; modes no longer carry permission.
@@ -726,6 +729,7 @@ pub(crate) async fn run_exec_agent(
.unwrap_or_else(codewhale_hooks::LifecycleOutbox::disabled);
// Wall clock for the outbox `turn_end` duration starts at the same
// `Op::SendMessage` boundary where `turn_start` is emitted below.
drop(startup_timing);
let exec_turn_started_at = Instant::now();
engine_handle
@@ -1194,6 +1198,7 @@ pub(crate) async fn run_exec_agent(
tool_catalog,
..
} => {
let _timing = codewhale_runtime::logging::PhaseTimer::start("exec_postprocessing");
let mut terminal_status = status;
let terminal_error = error;
#[cfg(unix)]
@@ -1421,6 +1426,10 @@ pub(crate) async fn run_exec_agent(
!summary.output.trim().is_empty(),
)
.unwrap_or_default();
let binary_sha256 = match binary_identity.take() {
Some(worker) => worker.await.ok().flatten(),
None => None,
};
emit_exec_stream_event(&ExecStreamEvent::Metadata {
meta: Box::new(ExecStreamMeta {
receipt_kind: "terminal",
@@ -1443,7 +1452,7 @@ pub(crate) async fn run_exec_agent(
retry_count: observed_retry_count,
approval_posture: approval_posture.clone(),
sandbox_posture: sandbox_posture.clone(),
binary_sha256: binary_sha256.clone(),
binary_sha256,
config_sha256: None,
prompt_sha256: prompt_sha256.clone(),
tool_catalog_sha256: tool_catalog.as_ref().and_then(|catalog| {
+8 -7
View File
@@ -14570,12 +14570,13 @@ fn tool_artifact_receipt(metadata: Option<&serde_json::Value>) -> Option<serde_j
(!artifact.is_empty()).then_some(serde_json::Value::Object(artifact))
}
fn current_binary_sha256() -> Option<String> {
let bytes = std::fs::read(std::env::current_exe().ok()?).ok()?;
Some(format!(
"sha256:{}",
codewhale_runtime::hashing::sha256_hex(&bytes)
))
fn start_binary_sha256() -> tokio::task::JoinHandle<Option<String>> {
let file = std::env::current_exe().and_then(std::fs::File::open);
tokio::task::spawn_blocking(move || {
let _timing = codewhale_runtime::logging::PhaseTimer::start("binary_identity");
let hash = codewhale_release::installs::reader_sha256(file.ok()?).ok()?;
Some(format!("sha256:{hash}"))
})
}
async fn run_workflow_tool_command(
@@ -14752,7 +14753,7 @@ async fn run_workflow_tool_command_inner(
retry_count: None,
approval_posture: "explicit_workflow_command".to_string(),
sandbox_posture: "configured".to_string(),
binary_sha256: current_binary_sha256(),
binary_sha256: start_binary_sha256().await.ok().flatten(),
config_sha256: None,
prompt_sha256: workflow_input_sha256,
tool_catalog_sha256: None,
+10 -2
View File
@@ -5502,12 +5502,20 @@ impl App {
/// lands within a turn-event of the real append. The shared snapshot is
/// installed without copying.
pub fn set_api_messages(&mut self, messages: Arc<Vec<Message>>) -> usize {
let unchanged = self
let exact = self
.api_messages
.iter()
.zip(messages.iter())
.take_while(|(old, new)| old == new)
.count();
let unchanged = exact
+ self.api_messages[exact..]
.iter()
.zip(&messages[exact..])
.take_while(|(old, new)| {
codewhale_models::turn_classify::same_recorded_turn(old, new)
})
.count();
self.api_message_stamps
.truncate(unchanged.min(self.api_message_stamps.len()));
self.api_message_stamps
@@ -5518,7 +5526,7 @@ impl App {
self.window_reroot_cause = Some("engine_projection");
}
self.api_messages = messages;
unchanged
exact
}
/// Install a resumed conversation, reusing the persisted journal's
+3 -2
View File
@@ -1500,6 +1500,7 @@ For every product-level `CODEWHALE_*` variable below, the matching legacy
`DEEPSEEK_*` name is still read as a compatibility fallback; when both are set,
the `CODEWHALE_*` value wins.
- `CODEWHALE_TIMING=1` emits local JSON phase durations on stderr for startup, model preparation, tool planning, approval waits and postprocessing. Nested intervals overlap. The records contain only phase, process ID, end time and elapsed microseconds; stream-json stdout is unchanged.
- `CODEWHALE_LOG_LEVEL` or `RUST_LOG` (`info`/`debug`/`trace` enables lightweight verbose logs)
- `CODEWHALE_SKILLS_DIR`
- `CODEWHALE_MCP_CONFIG`
@@ -2715,8 +2716,8 @@ reasoning contract, and all four membership ids omit generic sampling fields.
between portable `SKILL.md` bundles and Claude Code plugin runtimes.
- `[skills.bundled].install` (`"auto"` default, `"all"`, `"none"`): controls bundle writes independently of discovery. `auto` keeps deletion-as-opt-out and disables installation when `skills_dir` is set. `all` repairs enabled missing skills into `$CODEWHALE_HOME/skills` (normally `~/.codewhale/skills`) even with a discovery override. `none` writes nothing and leaves the marker untouched. Modified and managed bodies are preserved in every mode.
- `[skills.bundled].selected` / `disabled` / `explicit_only` (arrays of names, default `[]`): selection limits installation and discovery; empty selects the whole bundle. Disabled wins and reveals lower-root copies. Explicit-only retains user invocation while removing the shipped copy from the model index and model loading. Aliases share canonical policy. These settings affect recognized global bundle copies only and never delete files. Profile fields merge separately; `[]` clears an inherited list. `doctor` warns about unknown/ignored entries and `/skills inspect` reports per-path reasons. See [bundled skills](SKILLS.md#bundled-skills).
- `[skills].listing_max_description_chars` (integer, default `400`, range `1..=40000`): ambient-index description cap, shared by parent and child prompts and `/skills inspect`.
- `[skills].listing_budget_fraction` (finite number, default `0.05`, greater than `0` and at most `1`): share of the route window for that same index, estimated at four characters per token and clamped to `2400..=40000` characters. Explicit profile fields override inherited values independently; `/context` uses the same resolution. Full skill loading is unaffected.
- `[skills].listing_max_description_chars` (integer, default `120`, range `1..=40000`): ambient-index description cap, shared by parent and child prompts and `/skills inspect`.
- `[skills].listing_budget_fraction` (finite number, default `0.002`, greater than `0` and at most `1`): share of the route window for that same index, estimated at four characters per token and clamped to `2400..=40000` characters. Explicit profile fields override inherited values independently; `/context` uses the same resolution. Full skill loading is unaffected.
- `[skills].scan_codewhale_only` (bool, default `false`): when `true`, session
skill discovery ignores cross-tool roots such as `.claude/skills`,
`.opencode/skills`, `.cursor/skills`, and `~/.agents/skills`. Codewhale still
+14 -14
View File
@@ -18,7 +18,7 @@ and [CHANGELOG.md](../CHANGELOG.md).
421 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.
[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) · [DingYong4223](https://github.com/DingYong4223) · [djairjr](https://github.com/djairjr) · [dmitri-0](https://github.com/dmitri-0) · [dmt4](https://github.com/dmt4) · [donglovejava](https://github.com/donglovejava) · [douglarek](https://github.com/douglarek) · [Dr3259](https://github.com/Dr3259) · [DracheTek](https://github.com/DracheTek) · [drpars](https://github.com/drpars) · [dst1213](https://github.com/dst1213) · [duanchao-lab](https://github.com/duanchao-lab) · [Duducoco](https://github.com/Duducoco) · [dumbjack](https://github.com/dumbjack) · [dxfq](https://github.com/dxfq) · [dzyuan](https://github.com/dzyuan) · [e792a8](https://github.com/e792a8) · [Edouard-Legoupil](https://github.com/Edouard-Legoupil) · [elowen53](https://github.com/elowen53) · [eltociear](https://github.com/eltociear) · [EmiyaKiritsugu3](https://github.com/EmiyaKiritsugu3) · [encyc](https://github.com/encyc) · [eng2007](https://github.com/eng2007) · [eugenicum](https://github.com/eugenicum) · [EvanProgramming](https://github.com/EvanProgramming) · [F1LT3R](https:/Line truncated
[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) · [DingYong4223](https://github.com/DingYong4223) · [djairjr](https://github.com/djairjr) · [dmitri-0](https://github.com/dmitri-0) · [dmt4](https://github.com/dmt4) · [donglovejava](https://github.com/donglovejava) · [douglarek](https://github.com/douglarek) · [Dr3259](https://github.com/Dr3259) · [DracheTek](https://github.com/DracheTek) · [drpars](https://github.com/drpars) · [dst1213](https://github.com/dst1213) · [duanchao-lab](https://github.com/duanchao-lab) · [Duducoco](https://github.com/Duducoco) · [dumbjack](https://github.com/dumbjack) · [dxfq](https://github.com/dxfq) · [dzyuan](https://github.com/dzyuan) · [e792a8](https://github.com/e792a8) · [Edouard-Legoupil](https://github.com/Edouard-Legoupil) · [elowen53](https://github.com/elowen53) · [eltociear](https://github.com/eltociear) · [EmiyaKiritsugu3](https://github.com/EmiyaKiritsugu3) · [encyc](https://github.com/encyc) · [eng2007](https://github.com/eng2007) · [eugenicum](https://github.com/eugenicum) · [EvanProgramming](https://github.com/EvanProgramming) · [F1LT3R](https:/Line truncated
Also with us, from commits whose email is not linked to an account: AccMoment · AmosLeeeeeeee · artanis · Assassin-D007 · bluth · codgo · kurise · LeHaiDang · lei · liushiao · liuyang · Lê Hải Đăng · markchang · ningjingkun · ornn.ban · pinvou3-dev · sanbo · ts25504 · v_wanghuanping01 · wangfeng · xiedongsheng · z27014 · zero · Zhiping · Zhuoran Deng · ZZHAsus · 浩淼 · 郭闯.
<!-- contributor-wall:end -->
@@ -54,7 +54,7 @@ changelog or release notes, and this contributor record.
- **[gaord](https://github.com/gaord)** — a running turn's cost and context ([#7074](https://github.com/codewhale-hq/Codewhale/pull/7074)) and forks that measure the history they carry ([#7147](https://github.com/codewhale-hq/Codewhale/pull/7147)), and private state on a HarmonyOS mount that cannot express ownership ([#7168](https://github.com/codewhale-hq/Codewhale/pull/7168)).
- **[luochen211](https://github.com/luochen211)** — one form per term in the zh-Hans pack ([#7150](https://github.com/codewhale-hq/Codewhale/pull/7150)) and blanket dead-code allows dropped in fleet alerts and the pet worker ([#7149](https://github.com/codewhale-hq/Codewhale/pull/7149)).
- **[gannp](https://github.com/gannp)** — vLLM effort values that follow the local model template ([#7124](https://github.com/codewhale-hq/Codewhale/issues/7124), [#7125](https://github.com/codewhale-hq/Codewhale/pull/7125)).
- **[Sh1Zuku / SparkofSpike](https://github.com/SparkofSpike)** — relocated state root before session-side state ([#7097](https://github.com/codewhale-hq/Codewhale/pull/7097)) goal continuation that pauses to ask ([#7175](https://github.com/codewhale-hq/Codewhale/pull/7175)) and recall of the transcript compaction replaced ([#7173](https://github.com/codewhale-hq/Codewhale/pull/7173)). Goal-tool schema guidance for awaiting an answer (#7177).
- **[Sh1Zuku / SparkofSpike](https://github.com/SparkofSpike)** — relocated state root before session-side state ([#7097](https://github.com/codewhale-hq/Codewhale/pull/7097)) goal continuation that pauses to ask ([#7175](https://github.com/codewhale-hq/Codewhale/pull/7175)) and recall of the transcript compaction replaced ([#7173](https://github.com/codewhale-hq/Codewhale/pull/7173)). Also clarified goal-tool guidance while awaiting an answer ([#7177](https://github.com/codewhale-hq/Codewhale/pull/7177)) and kept star keycap graphemes intact through Markdown parsing ([#7176](https://github.com/codewhale-hq/Codewhale/pull/7176)). Their journal fix keeps a provisional input on the same session branch when the Engine stamps it ([#7180](https://github.com/codewhale-hq/Codewhale/pull/7180)).
</details>
@@ -68,7 +68,7 @@ changelog or release notes, and this contributor record.
- **[printf033](https://github.com/printf033)** — confined-file I/O on Android/Termux: SELinux-denied `open("/", O_RDONLY|O_DIRECTORY)` anchor replaced with an `O_PATH` walk (Linux-cfg-gated) plus a re-materialized final parent, restoring journal, compaction and artifact writes in search-only SELinux domains ([#6983](https://github.com/codewhale-hq/Codewhale/pull/6983)), with the diagnosis and on-device reproducer in [#6980](https://github.com/codewhale-hq/Codewhale/issues/6980). Source change; compiled Android and device acceptance remain unqualified.
- **[Paulo Aboim Pinto / aboimpinto](https://github.com/aboimpinto)** — moved config-command parsing and replies onto portable contracts while the host retains effects ([#6973](https://github.com/codewhale-hq/Codewhale/pull/6973)).
- **[Henrik Bakke Dukefoss / henrikdukefoss](https://github.com/henrikdukefoss)** — added the Standard Compute compatible-host descriptor, with live model discovery and its billing basis kept Unknown ([#6985](https://github.com/codewhale-hq/Codewhale/pull/6985)).
- **[Sh1Zuku / SparkofSpike](https://github.com/SparkofSpike)** — live network-policy updates and goal milestone hand-back ([#6928](https://github.com/codewhale-hq/Codewhale/pull/6928), [#6930](https://github.com/codewhale-hq/Codewhale/pull/6930)), and a Codewhale home reached through a symlink or junction ([#6947](https://github.com/codewhale-hq/Codewhale/issues/6947)). Also removed repeated catalog construction and credential-readiness work from the model picker ([#7026](https://github.com/codewhale-hq/Codewhale/pull/7026)). Goal-tool schema guidance for awaiting an answer (#7177).
- **[Sh1Zuku / SparkofSpike](https://github.com/SparkofSpike)** — live network-policy updates and goal milestone hand-back ([#6928](https://github.com/codewhale-hq/Codewhale/pull/6928), [#6930](https://github.com/codewhale-hq/Codewhale/pull/6930)), and a Codewhale home reached through a symlink or junction ([#6947](https://github.com/codewhale-hq/Codewhale/issues/6947)). Also removed repeated catalog construction and credential-readiness work from the model picker ([#7026](https://github.com/codewhale-hq/Codewhale/pull/7026)).
- **[dajiaohuang](https://github.com/dajiaohuang)** — cache-write token accounting, with missing and explicit-zero usage preserved ([#6913](https://github.com/codewhale-hq/Codewhale/pull/6913)).
- **[gaord](https://github.com/gaord)** — embedder-declared telemetry surface support without changing collection policy ([#6916](https://github.com/codewhale-hq/Codewhale/pull/6916)), and the DeepSeek saved-key marker diagnosis and fix ([#6972](https://github.com/codewhale-hq/Codewhale/issues/6972), [#6974](https://github.com/codewhale-hq/Codewhale/pull/6974)).
- **[Lstarsky0](https://github.com/Lstarsky0)** — localized `/profile` replies across all 15 complete locale packs ([#6919](https://github.com/codewhale-hq/Codewhale/pull/6919)), and workflow gates that block their own role rejected at plan admission ([#6952](https://github.com/codewhale-hq/Codewhale/pull/6952)). Also made feedback truncation readable in ASCII-safe mode, added a statusline scroll rail, and removed duplicate clipped descriptions from split-view hotbar setup ([#7027](https://github.com/codewhale-hq/Codewhale/pull/7027), [#7028](https://github.com/codewhale-hq/Codewhale/pull/7028), [#7029](https://github.com/codewhale-hq/Codewhale/pull/7029)).
@@ -116,7 +116,7 @@ changelog or release notes, and this contributor record.
- **[dajiaohuang](https://github.com/dajiaohuang)** — validated `config set` values against the settings schema ([#6568](https://github.com/codewhale-hq/Codewhale/pull/6568)).
- **[Water-Run](https://github.com/Water-Run)** — ingested namespaced model-only catalog entries so models present only in the canonical `models` map reach the offering list ([#6400](https://github.com/codewhale-hq/Codewhale/pull/6400)), and retired the blanket dead-code allowance and its unused feature stages, tightening the budget to match ([#6402](https://github.com/codewhale-hq/Codewhale/pull/6402)).
- **[wuisabel-gif](https://github.com/wuisabel-gif)** — designed the `tool_call_after` execution-receipt contract and its tests on a reference branch, which landed re-implemented on the current hook seam ([#6689](https://github.com/codewhale-hq/Codewhale/issues/6689), [#6713](https://github.com/codewhale-hq/Codewhale/pull/6713)).
- **[Sh1Zuku / SparkofSpike](https://github.com/SparkofSpike)** — let making room survive a provider request-body limit (HTTP 413) by shrinking, then replacing, inline images for that one summary pass ([#6642](https://github.com/codewhale-hq/Codewhale/pull/6642)). Also translated seventeen more guides into Simplified Chinese, among them plugins, memory, sandbox, workflow authoring, Docker, Termux and HarmonyOS, and brought the existing zh_hans docs onto one terminology standard ([#6662](https://github.com/codewhale-hq/Codewhale/pull/6662)), then translated thirteen developer and internal docs, among them the runtime API, architecture, tool surface, authorization order, operations runbook and Workroom security ([#6663](https://github.com/codewhale-hq/Codewhale/pull/6663)). Goal-tool schema guidance for awaiting an answer (#7177).
- **[Sh1Zuku / SparkofSpike](https://github.com/SparkofSpike)** — let making room survive a provider request-body limit (HTTP 413) by shrinking, then replacing, inline images for that one summary pass ([#6642](https://github.com/codewhale-hq/Codewhale/pull/6642)). Also translated seventeen more guides into Simplified Chinese, among them plugins, memory, sandbox, workflow authoring, Docker, Termux and HarmonyOS, and brought the existing zh_hans docs onto one terminology standard ([#6662](https://github.com/codewhale-hq/Codewhale/pull/6662)), then translated thirteen developer and internal docs, among them the runtime API, architecture, tool surface, authorization order, operations runbook and Workroom security ([#6663](https://github.com/codewhale-hq/Codewhale/pull/6663)).
**Reports and reproductions**
@@ -170,7 +170,7 @@ changelog or release notes, and this contributor record.
- **[gaord](https://github.com/gaord)** — contributed the file-scoped restore endpoint and trust-gated whole-tree rollback ([#6111](https://github.com/codewhale-hq/Codewhale/pull/6111)), Fleet schema inspection, role precedence and worker deliverable receipts, and linked the community VS Code frontend ([#5944](https://github.com/codewhale-hq/Codewhale/pull/5944), [#5945](https://github.com/codewhale-hq/Codewhale/pull/5945), [#5946](https://github.com/codewhale-hq/Codewhale/pull/5946), [#5992](https://github.com/codewhale-hq/Codewhale/pull/5992)).
- **[goransh-walia](https://github.com/goransh-walia)** — contributed the propose-only commit-planning rework ([#5870](https://github.com/codewhale-hq/Codewhale/pull/5870)).
- **[7jrxt42BxFZo4iAnN4CX](https://github.com/7jrxt42BxFZo4iAnN4CX)** — documented turn budgets and goal configuration, and reported gaps in command discovery, Fleet navigation, human waits, state hooks, history and provider routing ([#5996](https://github.com/codewhale-hq/Codewhale/pull/5996), [#5952](https://github.com/codewhale-hq/Codewhale/issues/5952), [#5954](https://github.com/codewhale-hq/Codewhale/issues/5954), [#6003](https://github.com/codewhale-hq/Codewhale/issues/6003), [#6004](https://github.com/codewhale-hq/Codewhale/issues/6004), [#6006](https://github.com/codewhale-hq/Codewhale/issues/6006), [#6007](https://github.com/codewhale-hq/Codewhale/issues/6007)).
- **[SparkofSpike](https://github.com/SparkofSpike)** — contributed two-stage consent for opting out of model-bound credential redaction and session titles derived from real user prompts instead of runtime handoffs ([#5982](https://github.com/codewhale-hq/Codewhale/pull/5982), [#6012](https://github.com/codewhale-hq/Codewhale/pull/6012)). Goal-tool schema guidance for awaiting an answer (#7177).
- **[SparkofSpike](https://github.com/SparkofSpike)** — contributed two-stage consent for opting out of model-bound credential redaction and session titles derived from real user prompts instead of runtime handoffs ([#5982](https://github.com/codewhale-hq/Codewhale/pull/5982), [#6012](https://github.com/codewhale-hq/Codewhale/pull/6012)).
- **[aboimpinto](https://github.com/aboimpinto)** — moved session lifecycle and session-control commands onto shared command contracts ([#5902](https://github.com/codewhale-hq/Codewhale/pull/5902), [#5951](https://github.com/codewhale-hq/Codewhale/pull/5951)).
- **[EvanProgramming](https://github.com/EvanProgramming)** — reported Windows input and CRLF-write defects, and contributed CRLF preservation and an injectable Windows input runner ([#5908](https://github.com/codewhale-hq/Codewhale/issues/5908), [#5909](https://github.com/codewhale-hq/Codewhale/issues/5909), [#5910](https://github.com/codewhale-hq/Codewhale/pull/5910), [#5911](https://github.com/codewhale-hq/Codewhale/pull/5911), [#5912](https://github.com/codewhale-hq/Codewhale/pull/5912)).
- **[wuisabel-gif](https://github.com/wuisabel-gif)** — exposed workspace file suggestions through the Runtime API ([#6120](https://github.com/codewhale-hq/Codewhale/pull/6120)) and added custom-theme discovery, preview and selection in the theme picker ([#5907](https://github.com/codewhale-hq/Codewhale/pull/5907)).
@@ -220,7 +220,7 @@ changelog or release notes, and this contributor record.
(#5689)
- **[gaord](https://github.com/gaord)** — added the `GET /v1/fleet/profiles`
runtime API endpoint, reusing the FleetManager validation path (#5688)
- **[Sh1Zuku / SparkofSpike](https://github.com/SparkofSpike)** — corrected Goal-tool schema guidance for awaiting an answer (#7177).
- **[Sh1Zuku / SparkofSpike](https://github.com/SparkofSpike)** — corrected
English documentation inaccuracies and contributed the first zh_hans
translations for the Tier-2 docs (#5613)
- **[M-Maciej](https://github.com/M-Maciej)** — goal continuation cadence fix
@@ -311,7 +311,7 @@ changelog or release notes, and this contributor record.
**Merged or adapted contributions**
- **[Sh1Zuku / SparkofSpike](https://github.com/SparkofSpike)** — restored Goal-tool schema guidance for awaiting an answer (#7177).
- **[Sh1Zuku / SparkofSpike](https://github.com/SparkofSpike)** — restored
`/title` as an independent, persistent terminal-window title in PR #5509,
and contributed the Tier 1 Chinese and Indonesian documentation move to
`docs/zh_hans/` and `docs/id/` with one-release redirect stubs (#5507,
@@ -383,7 +383,7 @@ changelog or release notes, and this contributor record.
**Merged or adapted contributions**
- **[Lstarsky0](https://github.com/Lstarsky0)** — compact rows no longer reserve a hidden session-metrics strip ([#5486](https://github.com/codewhale-hq/Codewhale/pull/5486)); docs shell and shared component pick-text onto the typed dictionary spine ([#5488](https://github.com/codewhale-hq/Codewhale/pull/5488), [#5490](https://github.com/codewhale-hq/Codewhale/pull/5490))
- **[Sh1Zuku / SparkofSpike](https://github.com/SparkofSpike)** — topbar session title ([#5481](https://github.com/codewhale-hq/Codewhale/pull/5481)) Goal-tool schema guidance for awaiting an answer (#7177).
- **[Sh1Zuku / SparkofSpike](https://github.com/SparkofSpike)** — topbar session title ([#5481](https://github.com/codewhale-hq/Codewhale/pull/5481))
- **[h3c-hexin](https://github.com/h3c-hexin)** — a concrete route/offering
output limit now outranks the conservative 8,192-token compatibility guess
for an uncatalogued model; routes that publish no limit stay fail-closed
@@ -435,7 +435,7 @@ changelog or release notes, and this contributor record.
reasoning effort mapped to every provider's maximum tier (#5409); session
titles truncated by character count instead of byte offset so CJK and
emoji names cut at the intended width (#5415)
- **[SparkofSpike](https://github.com/SparkofSpike)** — `/title` and the Goal-tool schema guidance for awaiting an answer (#7177).
- **[SparkofSpike](https://github.com/SparkofSpike)** — `/title` and the
saved session name in the terminal tab/window title, plus the fix for the
deadlock when the title changed during an active turn; landed as a
discoverable alias of `/rename` on the one persisted session name (#5419)
@@ -499,7 +499,7 @@ changelog or release notes, and this contributor record.
**Merged or adapted contributions**
- **[Sh1Zuku / SparkofSpike](https://github.com/SparkofSpike)** — stale cached Goal-tool schema guidance for awaiting an answer (#7177).
- **[Sh1Zuku / SparkofSpike](https://github.com/SparkofSpike)** — stale cached
session-title recovery so `New Session` yields to the first real prompt
- **[Paulo Aboim Pinto / aboimpinto](https://github.com/aboimpinto)** — the
shared alias-aware command discovery contract across the palette, slash
@@ -516,7 +516,7 @@ changelog or release notes, and this contributor record.
**Merged or adapted contributions**
- **[Sh1Zuku / SparkofSpike](https://github.com/SparkofSpike)** — LaTeX Goal-tool schema guidance for awaiting an answer (#7177).
- **[Sh1Zuku / SparkofSpike](https://github.com/SparkofSpike)** — LaTeX
rendering in PR #4981, circled-digit and keycap width handling in PR #5001,
and actionable File-edit recovery in PR #5008; sub-agent checkpoint resume
in PR #5242, real shell wait elapsed time in PR #5240, and keeping
@@ -575,7 +575,7 @@ changelog or release notes, and this contributor record.
model-aware route, authentication, documentation, and test-isolation work
from closed PR #4467, whose contributor commits are preserved in the
candidate
- **[Sh1Zuku / SparkofSpike](https://github.com/SparkofSpike)** — the zh-Hans Goal-tool schema guidance for awaiting an answer (#7177).
- **[Sh1Zuku / SparkofSpike](https://github.com/SparkofSpike)** — the zh-Hans
translation quality review harvested from PR #4908, duplicate-session fix in
PR #4963, LaTeX implementation from PR #4973 landed through #4974, and the
context-menu reproduction in #4803
@@ -682,7 +682,7 @@ changelog or release notes, and this contributor record.
archive support (#4267)
- **[stream2stream](https://github.com/stream2stream)** — the legacy-session
recovery report that led to the read-only doctor diagnostic (#4032, #4539)
- **[Sh1Zuku / SparkofSpike](https://github.com/SparkofSpike)** — the Windows Goal-tool schema guidance for awaiting an answer (#7177).
- **[Sh1Zuku / SparkofSpike](https://github.com/SparkofSpike)** — the Windows
Ctrl+O reproduction that exposed pre-pager result truncation and conflicting
composer shortcut routing (#4482)
- **[Lu Shihan / Angel-Hair](https://github.com/Angel-Hair)** — session-owned
@@ -693,7 +693,7 @@ changelog or release notes, and this contributor record.
- **[Fred Leitz / fleitz](https://github.com/fleitz)** — canonical `Bash`
no-`cwd` workspace resolution and its regression test, keeping isolated
sub-agent commands inside their selected worktree (PR #4673; issue #4674)
- **[Sh1Zuku / SparkofSpike](https://github.com/SparkofSpike)** — exact Goal-tool schema guidance for awaiting an answer (#7177).
- **[Sh1Zuku / SparkofSpike](https://github.com/SparkofSpike)** — exact
Vim-space regression reproduction and verification that the v0.9.1 input
path already contains the needed global binding (PR #4477)
+5 -5
View File
@@ -261,8 +261,8 @@ Collision and prompt-budget invariants asserted today:
| Canonical wins | A canonical bundled name always beats another skill's alias (`docx` → `docx`, never `documents`). |
| Single alias owner | No two bundled skills may claim the same alias. |
| No duplicate entries | Each canonical name renders at most one catalogue line; aliases render zero. |
| Budget headroom | The shipped pack alone renders under the window-scaled skills budget (25 600 chars at the default 128k window; 2 400-char floor) with **no** "additional skills omitted" line, so user skills are never silently displaced. |
| No context poisoning | Descriptions stay single-line and respect `[skills] listing_max_description_chars` (default 400) before entering the prompt. |
| Budget headroom | The shipped pack alone renders under the window-scaled skills budget (2,400 characters at the default 128k window, the index floor) with **no** "additional skills omitted" line, so user skills are never silently displaced. |
| No context poisoning | Descriptions stay single-line and respect `[skills] listing_max_description_chars` (default 120) before entering the prompt. |
### Locale-aware routing metadata
@@ -360,7 +360,7 @@ does not run readiness probes and does not block mutations on readiness.
## Config knobs
Parents, fresh and nested children, and `/context` use the same ambient-index renderer. `[skills] listing_max_description_chars` controls the per-entry ceiling (1–40,000; default 400). `listing_budget_fraction` controls the share of the resolved route window (greater than 0 and at most 1; default 0.05), estimated at four characters per token. The total index retains a 2,400-character floor and 40,000-character ceiling. Explicit profile values override inherited values independently. Changing a route recomputes its budget; it does not silently change the description setting.
Parents, fresh and nested children, and `/context` use the same ambient-index renderer. `[skills] listing_max_description_chars` controls the per-entry ceiling (1–40,000; default 120). `listing_budget_fraction` controls the share of the resolved route window (greater than 0 and at most 1; default 0.002), estimated at four characters per token. The total index retains a 2,400-character floor and 40,000-character ceiling. Explicit profile values override inherited values independently. Changing a route recomputes its budget; it does not silently change the description setting.
The renderer favors `Use when:` triggers, shortens descriptions before falling back to names, and reports shortened/omitted entries. Descriptions exceeding the configured cap without a trigger also produce a load warning. `/skills inspect` shows a localized note with the same effective cap, and `/context` repeats the loss count. `load_skill name="list"` and exact-name loading retain full text. These limits govern ambient routing metadata, not skill bodies or external MCP resource descriptions.
@@ -373,8 +373,8 @@ skills_dir = "/path/to/skills"
# When true, runtime discovery skips cross-tool roots (.claude, .agents, …).
# Owned Codewhale roots and an explicit skills_dir override still apply.
scan_codewhale_only = false
listing_max_description_chars = 400
listing_budget_fraction = 0.05
listing_max_description_chars = 120
listing_budget_fraction = 0.002
# Optional registry / install size overrides used by --remote, sync, and install.
# registry_url = "https://…"
+3 -2
View File
@@ -125,8 +125,7 @@ dependencies are available.
MCP tools are dynamic. Successfully connected servers register names such as
`mcp_<server>_<tool>` from `~/.codewhale/mcp.json`; a failed or disabled server
must not be presented as available. MCP and plugin tools are deferred unless a
user explicitly names them in `[tools].always_load`.
must not be presented as available. MCP and plugin tools are normally deferred. When the composition VM is eager and the turn admits exactly one compact named batch schema, `execute_tools` also accepts `{batch:{tool,arguments}}` instead of JavaScript. The VM forwards the data through the same nested gate, bounds and receipts; the server tool stays deferred unless explicitly requested. The current server routes and turn permissions narrow the shortcut’s member names; its nested dispatch checks the offered members after hooks. The server supplies a bounded guide to their arguments. Other tools stay searchable; `[tools].always_load` still makes individual schemas eager.
For an unstarted configured server, an MCP-focused `tool_search` first
performs bounded discovery under the current turn's server/tool ceiling.
@@ -138,6 +137,8 @@ unstarted. The CLI's standalone MCP inspection commands own a separate pool.
### Code mode (`execute_tools`)
For a known sequence, prefer a tool’s own batch operation. Use `execute_tools` when the next call depends on an intermediate result, and return the verified outcome. Search unknown names and schemas before acting. An unexpected state, refusal, uncertain delivery or direct-only decision ends that sequence.
`execute_tools` is engine-injected, alongside the synthetic interpreter tools.
It runs a JavaScript program whose only host surface is
`await tools.call(name, args)`, and it is the default way to compose several
+2 -2
View File
@@ -1333,8 +1333,8 @@ DeepSeek V4 前缀缓存让 token 标签变得重要。这些数量保持分离
- `[skills.bundled].install`(默认 `"auto"`,另可设 `"all"` 或 `"none"`):独立控制捆绑安装。`auto` 保留删除即退出规则,设置 `skills_dir` 时停止安装;`all` 补装已启用的缺失技能,只写入 `$CODEWHALE_HOME/skills`(通常为 `~/.codewhale/skills`),不受发现目录覆盖影响;`none` 不写文件或版本标记。修改版和托管正文始终保留。
- `[skills.bundled].selected` / `disabled` / `explicit_only`(名称数组,默认 `[]`):空选择列表启用整个安装包,非空列表限制安装和发现。禁用优先,并允许低优先级目录的同名副本生效。显式调用保留用户调用,移除模型索引及模型加载权限。别名沿用规范名称策略;仅影响全局自有目录中可识别的发行副本,不删除文件。配置档逐字段合并,显式空列表清空继承值。`doctor` 报告未知及忽略条目,`/skills inspect` 显示路径和原因。详见 [SKILLS.md](SKILLS.md#捆绑技能)。
- `[skills].listing_max_description_chars`(整数,默认 `400`,范围 `1..=40000`):父代理、子代理提示及 `/skills inspect` 共用的索引描述上限。
- `[skills].listing_budget_fraction`(有限数值,默认 `0.05`,大于 `0` 且不大于 `1`):同一索引占路由窗口的比例,按每 token 四个字符估算,并限制在 `2400..=40000` 字符。配置档中的字段分别覆盖继承值;`/context` 使用相同解析。完整技能加载不受影响。
- `[skills].listing_max_description_chars`(整数,默认 `120`,范围 `1..=40000`):父代理、子代理提示及 `/skills inspect` 共用的索引描述上限。
- `[skills].listing_budget_fraction`(有限数值,默认 `0.002`,大于 `0` 且不大于 `1`):同一索引占路由窗口的比例,按每 token 四个字符估算,并限制在 `2400..=40000` 字符。配置档中的字段分别覆盖继承值;`/context` 使用相同解析。完整技能加载不受影响。
- `[skills].scan_codewhale_only`(bool,默认 `false`):为 `true` 时,会话技能发现忽略跨工具根,如 `.claude/skills`、`.opencode/skills`、`.cursor/skills` 和 `~/.agents/skills`。Codewhale 仍扫描 `<workspace>/.codewhale/skills`、`~/.codewhale/skills` 和任何显式 `skills_dir` 覆盖。技能管理器仍可独立于这个运行时旋钮切换本地兼容审计扫描——见 [SKILLS.md](SKILLS.md)。
- `[skills].registry_url` / `[skills].max_install_size_bytes`(可选):`/skills --remote`、`/skills suggest <task>`、`/skills sync` 和 `/skill install|update` 使用。默认管理器打开路径不联系 registry。
+5 -5
View File
@@ -184,8 +184,8 @@ ASCII 名称保留现有命令拼写。包含非 ASCII 字符的名称会获得
| 规范优先 | 规范内置名称总是胜过另一个 skill 的别名(`docx` → `docx`,绝不会是 `documents`)。 |
| 单一别名所有者 | 不得有两个内置 skills 声称同一别名。 |
| 无重复条目 | 每个规范名称最多渲染一行目录;别名渲染零行。 |
| 预算余量 | 随附包单独渲染时低于随窗口缩放的 skills 预算(默认 128k 窗口下为 25 600 字符,下限 2 400 字符),且**没有**“additional skills omitted”行,因此用户 skills 永远不会被静默挤掉。 |
| 无上下文污染 | 描述保持单行,并在进入提示前被截断到 配置的 `listing_max_description_chars`(默认 400)。 |
| 预算余量 | 随附包单独渲染时低于随窗口缩放的 skills 预算(默认 128k 窗口下为 2,400 字符,即索引下限),且**没有**“additional skills omitted”行,因此用户 skills 永远不会被静默挤掉。 |
| 无上下文污染 | 描述保持单行,并在进入提示前被截断到 配置的 `listing_max_description_chars`(默认 120)。 |
### 区域感知的路由元数据
@@ -261,7 +261,7 @@ ASCII 名称保留现有命令拼写。包含非 ASCII 字符的名称会获得
## 配置项
父代理、新建或嵌套子代理及 `/context` 使用同一个环境索引渲染器。`[skills] listing_max_description_chars` 设置单条描述上限(1–40,000,默认 400);`listing_budget_fraction` 设置索引占当前路由上下文窗口的比例(大于 0 且不大于 1,默认 0.05),按每 token 四个字符估算。总索引保留 2,400 字符下限和 40,000 字符上限。配置档中的两个值分别覆盖继承值。切换路由会重算预算,不会暗中修改描述上限。
父代理、新建或嵌套子代理及 `/context` 使用同一个环境索引渲染器。`[skills] listing_max_description_chars` 设置单条描述上限(1–40,000,默认 120);`listing_budget_fraction` 设置索引占当前路由上下文窗口的比例(大于 0 且不大于 1,默认 0.002),按每 token 四个字符估算。总索引保留 2,400 字符下限和 40,000 字符上限。配置档中的两个值分别覆盖继承值。切换路由会重算预算,不会暗中修改描述上限。
渲染器优先保留 `Use when:` 触发条件,先缩短描述,再退到仅名称,并报告缩短或省略的条目。超过配置上限且没有触发条件的描述会产生加载警告。`/skills inspect` 显示含实际上限的本地化提示,`/context` 显示同一损失计数。`load_skill name="list"` 和按名称加载仍返回完整文本。这些限制仅控制环境索引,不截断技能正文或外部 MCP 资源描述。
@@ -273,8 +273,8 @@ skills_dir = "/path/to/skills"
# 为 true 时,运行时发现会跳过跨工具根目录(.claude、.agents 等)。
# 拥有方 Codewhale 根目录与显式 skills_dir 覆盖项仍然生效。
scan_codewhale_only = false
listing_max_description_chars = 400
listing_budget_fraction = 0.05
listing_max_description_chars = 120
listing_budget_fraction = 0.002
# 仅在明确需要把项目顶层 skills/ 当作技能目录时启用。
flat_workspace_root = false
+18 -18
View File
@@ -32,8 +32,8 @@
"sources": [
{
"path": "docs/CONFIGURATION.md",
"sha256": "8497b51cee60446c7532b7cb00ecfb475a6db2f2c63ff05513ba0bda626f523e",
"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": "e98c48c12a1706233342bb483aea239dabce283fc3851adf10ea0038fd8f93b6",
"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",
@@ -64,8 +64,8 @@
"sources": [
{
"path": "docs/CONFIGURATION.md",
"sha256": "8497b51cee60446c7532b7cb00ecfb475a6db2f2c63ff05513ba0bda626f523e",
"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": "e98c48c12a1706233342bb483aea239dabce283fc3851adf10ea0038fd8f93b6",
"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",
@@ -96,8 +96,8 @@
},
{
"path": "docs/CONFIGURATION.md",
"sha256": "8497b51cee60446c7532b7cb00ecfb475a6db2f2c63ff05513ba0bda626f523e",
"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": "e98c48c12a1706233342bb483aea239dabce283fc3851adf10ea0038fd8f93b6",
"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
}
]
},
@@ -107,8 +107,8 @@
"sources": [
{
"path": "docs/TOOL_SURFACE.md",
"sha256": "419bb600a25de7ec3931e139fa2019d899eb1513413ae726b737f0629d6ba7ba",
"content": "# Tool surface\n\n> 阅读简体中文版:[zh_hans/TOOL_SURFACE.md](zh_hans/TOOL_SURFACE.md)。\n\nThis document describes the current model-facing tool contract. The v0.9.1\ncutover that produced it is recorded in `docs/RUNTIME_SIMPLIFICATION_DESIGN.md`;\nread the workspace version from `Cargo.toml`, not from this line. The registry\nremains larger than the first-turn catalog so\nsaved transcripts can replay and uncommon capabilities can be loaded on demand.\nThe model should learn one canonical name for each common operation.\n\nImplementation sources:\n\n- `crates/engine/src/core/engine/tool_catalog.rs` owns the eager/deferred catalog.\n- `crates/engine/src/tools/registry.rs` registers canonical tools and hidden aliases.\n- `crates/engine/src/tools/{file,file_tool,shell}.rs` own the small foreground\n primitive behavior and schemas; the other native tools remain searchable.\n- `docs/RUNTIME_SIMPLIFICATION_DESIGN.md` records the v0.9.1 cutover and receipt.\n\n## Default-active contract\n\nNew turns start with fifteen eager native names plus synthetic `tool_search`:\n\n1. `read`\n2. `write`\n3. `edit`\n4. `file_search`\n5. `bash`\n6. `agent`\n7. `workflow`\n8. `todo_write`\n9. `create_goal`\n10. `get_goal`\n11. `update_goal`\n12. `task_shell_wait`\n13. `load_skill`\n14. `context_archive_get`\n15. `context_archive_search`\n16. `tool_search` (synthetic, always active)\n\nThe fifteen native names are `DEFAULT_ACTIVE_NATIVE_TOOLS` in\n`crates/engine/src/core/engine/tool_catalog.rs`, pinned by\n`default_active_contract_keeps_discovery_and_core_tools_eager`. An authority\nboundary may remove `agent` at the maximum child depth, and Plan registers only\nthe foreground shell surface (so no `task_shell_wait`), but route size alone\nmust not change this core vocabulary.\n\nThe direct schemas deliberately stay small:\n\n| Tool | Input | Purpose |\n|---|---|---|\n| `read` | `path`, optional `offset`, optional `limit` | Read a bounded file window with explicit continuation or truncation notices. |\n| `write` | `path`, `content` | Create or replace a file. |\n| `edit` | `path`, `edits` | Apply one or more unambiguous text replacements against one original snapshot. |\n| `file_search` | `query`, optional `path`, `limit`, `extensions`, `exclude` | Find files by name, path fragment or glob and return bounded, ranked matches. Respects ignore files; `exclude` replaces default exclusions. |\n| `bash` | `command`, optional `timeout`, optional `description` | Run one cancellable foreground shell command and return a bounded tail. The optional description is display-only. |\n| `agent` | delegated task and optional scope/context controls | Start or inspect focused child work. |\n| `workflow` | plan/script/source_path plus run controls | Coordinate multi-agent phases with dependencies and completion checks. |\n| `todo_write` | complete replacement list of `{content, status}` items | Keep optional, agent-owned progress notes for genuinely multi-step work. |\n| `create_goal` | objective plus optional budget | Start the session goal the turn works toward. |\n| `get_goal` | none | Read the active goal and its progress. |\n| `update_goal` | terminal status | Mark the goal complete or blocked. |\n| `task_shell_wait` | `task_id`, optional `wait`, `timeout_ms`, `cancel`, `gate` | Poll or stop a background shell task. Completion still arrives as a runtime event, so use it for early output, true barriers, or gate evidence. |\n| `load_skill` | optional `name`, `query` | Load a named skill's `SKILL.md` body and companion-file list; omit `name` or use `name=\"list\"` to list eligible skills, or use `query` to search names and descriptions. |\n| `tool_search` | `query`, optional matching controls | Discover policy-allowed deferred tools and add selected schemas to this conversation's toolbox. |\n\nMode is an authority decision, not a synonym system. Plan, Work, and Operate\nuse the same primitive identities. Plan centrally refuses `write`, `edit`, and\n`bash`; Work and Operate still pass those calls through approval, sandbox,\ntrusted-path, repository-law, and managed-policy gates. Full Access changes\nordinary approval behavior but does not bypass hard safety or repository law.\n\n`update_plan` remains registered only for saved-artifact compatibility and is\nnot model-visible. `tasks`, `Git`, `Run`, `Web`, `remember`, and other\nspecialized capabilities are searchable rather than first-turn ceremony.\n\nOn the TUI's default (calm) shell card the Bash description is the headline,\nshown as written apart from stripped terminal escapes, collapsed whitespace and\na length bound, and the command sits on its own `$` row directly under it in\nevery state and at every width, including automatic runs. A card without a\ndescription leads with the `$` command instead. The labeled card\n(`show_tool_details = true` with `calm_mode = false`) keeps the command in its\nheader and the description as a muted `note:` row. The description nLine truncated
"sha256": "915fd375ed479c107ee46a52a8ebb941b5aec4f39473a1c297d5344de697bce7",
"content": "# Tool surface\n\n> 阅读简体中文版:[zh_hans/TOOL_SURFACE.md](zh_hans/TOOL_SURFACE.md)。\n\nThis document describes the current model-facing tool contract. The v0.9.1\ncutover that produced it is recorded in `docs/RUNTIME_SIMPLIFICATION_DESIGN.md`;\nread the workspace version from `Cargo.toml`, not from this line. The registry\nremains larger than the first-turn catalog so\nsaved transcripts can replay and uncommon capabilities can be loaded on demand.\nThe model should learn one canonical name for each common operation.\n\nImplementation sources:\n\n- `crates/engine/src/core/engine/tool_catalog.rs` owns the eager/deferred catalog.\n- `crates/engine/src/tools/registry.rs` registers canonical tools and hidden aliases.\n- `crates/engine/src/tools/{file,file_tool,shell}.rs` own the small foreground\n primitive behavior and schemas; the other native tools remain searchable.\n- `docs/RUNTIME_SIMPLIFICATION_DESIGN.md` records the v0.9.1 cutover and receipt.\n\n## Default-active contract\n\nNew turns start with fifteen eager native names plus synthetic `tool_search`:\n\n1. `read`\n2. `write`\n3. `edit`\n4. `file_search`\n5. `bash`\n6. `agent`\n7. `workflow`\n8. `todo_write`\n9. `create_goal`\n10. `get_goal`\n11. `update_goal`\n12. `task_shell_wait`\n13. `load_skill`\n14. `context_archive_get`\n15. `context_archive_search`\n16. `tool_search` (synthetic, always active)\n\nThe fifteen native names are `DEFAULT_ACTIVE_NATIVE_TOOLS` in\n`crates/engine/src/core/engine/tool_catalog.rs`, pinned by\n`default_active_contract_keeps_discovery_and_core_tools_eager`. An authority\nboundary may remove `agent` at the maximum child depth, and Plan registers only\nthe foreground shell surface (so no `task_shell_wait`), but route size alone\nmust not change this core vocabulary.\n\nThe direct schemas deliberately stay small:\n\n| Tool | Input | Purpose |\n|---|---|---|\n| `read` | `path`, optional `offset`, optional `limit` | Read a bounded file window with explicit continuation or truncation notices. |\n| `write` | `path`, `content` | Create or replace a file. |\n| `edit` | `path`, `edits` | Apply one or more unambiguous text replacements against one original snapshot. |\n| `file_search` | `query`, optional `path`, `limit`, `extensions`, `exclude` | Find files by name, path fragment or glob and return bounded, ranked matches. Respects ignore files; `exclude` replaces default exclusions. |\n| `bash` | `command`, optional `timeout`, optional `description` | Run one cancellable foreground shell command and return a bounded tail. The optional description is display-only. |\n| `agent` | delegated task and optional scope/context controls | Start or inspect focused child work. |\n| `workflow` | plan/script/source_path plus run controls | Coordinate multi-agent phases with dependencies and completion checks. |\n| `todo_write` | complete replacement list of `{content, status}` items | Keep optional, agent-owned progress notes for genuinely multi-step work. |\n| `create_goal` | objective plus optional budget | Start the session goal the turn works toward. |\n| `get_goal` | none | Read the active goal and its progress. |\n| `update_goal` | terminal status | Mark the goal complete or blocked. |\n| `task_shell_wait` | `task_id`, optional `wait`, `timeout_ms`, `cancel`, `gate` | Poll or stop a background shell task. Completion still arrives as a runtime event, so use it for early output, true barriers, or gate evidence. |\n| `load_skill` | optional `name`, `query` | Load a named skill's `SKILL.md` body and companion-file list; omit `name` or use `name=\"list\"` to list eligible skills, or use `query` to search names and descriptions. |\n| `tool_search` | `query`, optional matching controls | Discover policy-allowed deferred tools and add selected schemas to this conversation's toolbox. |\n\nMode is an authority decision, not a synonym system. Plan, Work, and Operate\nuse the same primitive identities. Plan centrally refuses `write`, `edit`, and\n`bash`; Work and Operate still pass those calls through approval, sandbox,\ntrusted-path, repository-law, and managed-policy gates. Full Access changes\nordinary approval behavior but does not bypass hard safety or repository law.\n\n`update_plan` remains registered only for saved-artifact compatibility and is\nnot model-visible. `tasks`, `Git`, `Run`, `Web`, `remember`, and other\nspecialized capabilities are searchable rather than first-turn ceremony.\n\nOn the TUI's default (calm) shell card the Bash description is the headline,\nshown as written apart from stripped terminal escapes, collapsed whitespace and\na length bound, and the command sits on its own `$` row directly under it in\nevery state and at every width, including automatic runs. A card without a\ndescription leads with the `$` command instead. The labeled card\n(`show_tool_details = true` with `calm_mode = false`) keeps the command in its\nheader and the description as a muted `note:` row. The description nLine truncated
},
{
"path": "docs/TOOL_LIFECYCLE.md",
@@ -224,8 +224,8 @@
},
{
"path": "docs/CONFIGURATION.md",
"sha256": "8497b51cee60446c7532b7cb00ecfb475a6db2f2c63ff05513ba0bda626f523e",
"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": "e98c48c12a1706233342bb483aea239dabce283fc3851adf10ea0038fd8f93b6",
"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
}
]
},
@@ -235,8 +235,8 @@
"sources": [
{
"path": "docs/SKILLS.md",
"sha256": "c045b4d8f63da3c3d77b028834ffa6b121a55c3a2d1da524a98bcc5139a16c5c",
"content": "# Skills Manager\n\nIn the terminal, `/skills` opens **Extensions → Skills**. Enter on a skill opens the dedicated manager, also available directly through `/skills manage`, for existing install, update, remove, and trust operations. `/skill <name>` still activates a skill, and the explicit inspection, remote, sync, and suggestion subcommands remain available.\n\n\n> 阅读简体中文版:[zh_hans/SKILLS.md](zh_hans/SKILLS.md)\n\nSkills are reusable `SKILL.md` instruction packs. Codewhale discovers them from\nseveral roots, but **only Codewhale-owned directories are writable**. The unified\n`/skills manage` manager is the interactive surface for audit and mutation; slash\naliases share the same write path.\n\nFor Claude Code plugin boundaries, see [CLAUDE_PLUGIN_COMPAT.md](CLAUDE_PLUGIN_COMPAT.md).\nFor `skills_dir` and `[skills]` config keys, see [CONFIGURATION.md](CONFIGURATION.md).\n\n## Architecture (four layers)\n\n| Layer | Role |\n| --- | --- |\n| **Root catalog** | Single source of precedence and ownership (`SkillRootCatalog`). |\n| **Audit** | Read-only, unmerged on-disk inventory (status, digest, actions). |\n| **Mutation controller** | Only writer for install / import / update / remove / trust. |\n| **Skills manager view** | TUI: emits events only; never writes files itself. |\n\nRuntime discovery (`SkillRegistry`) still merges skills for the model. Audit\nintentionally does **not** merge — it shows every on-disk copy so conflicts and\nshadowing stay visible.\n\n## Ownership and roots\n\n**Writable (Codewhale-owned)**\n\n| Scope | Path |\n| --- | --- |\n| Project | `<workspace>/.codewhale/skills/` |\n| Global | `~/.codewhale/skills/` |\n\n**Read-only compatible** (discover / import source only — never mutated in place)\n\nExamples: `<workspace>/.agents/skills`, `./skills`, `.claude/skills`,\n`.cursor/skills`, `.opencode/skills`, `~/.agents/skills`, `~/.claude/skills`,\nand similar harness layouts.\n\n**Audit-only (not runtime-active)**\n\n- Flat `<workspace>/skills` is an audit candidate until `[skills] flat_workspace_root = true`; an explicit `skills_dir` can also select it.\n- `.codex/skills` appears in **compatible** audit scans so operators can see it.\n It does **not** join the runtime discovery set.\n\nConfigured `skills_dir` that is not one of the owned Codewhale roots stays\nread-only. Discovery and the manager can list it; mutations still target owned\nproject/global roots only.\n\nWithin each scope, the owned `.codewhale/skills` root wins name collisions.\nProject order is `.codewhale`, `.agents`, `.claude`, `.opencode`, `.cursor`, then\nan opted-in flat `skills` root. Global order is `.codewhale`, `.agents`, `.claude`,\nthen legacy `.deepseek`. Project roots precede global roots. Shadowing warnings\nname both copies, so install and update select the owned copy in that scope.\n\nEvery root inside the workspace — owned, compatible, or a configured\n`skills_dir` that resolves there — loads only once the workspace is trusted\n(`/trust on --save`). Until then discovery names the skipped directories in its\nwarning, and the session's skills directory falls back to the global one.\n\n## Slash commands\n\n| Command | Behavior |\n| --- | --- |\n| `/skills` | Opens Extensions → Skills (owned-only scan, **no network**). |\n| `/skills manage` | Opens the dedicated Skills Manager for audit and mutation. |\n| `/skills <prefix>` | Text list filtered by name prefix. |\n| `/skills inspect` | Text discovery mode, searched directories, and source paths. |\n| `/skills --remote` | Explicit registry listing (network). |\n| `/skills suggest <task>` | Rank up to three remote skills for a task, with matching evidence and an explicit install command (network; no install). |\n| `/skills sync` | Explicit registry → local cache sync (network). |\n| `/skill <name>` | Activate a skill for the next turn. |\n| `/skill install [--project\\|--global] <spec>` | Install via mutation controller. |\n| `/skill update [--project\\|--global] <name>` | Update a managed skill from its registry provenance. |\n| `/skill uninstall [--project\\|--global] <name>` | Remove a managed skill. |\n| `/skill trust [--project\\|--global] <name>` | Write digest-bound advisory trust. |\n\nNotes:\n\n- There is **no** `/skills audit` subcommand. Use the manager (and `c` to toggle\n compatible roots) or `/skills inspect` for discovery details.\n- Bare `/skill install <spec>` (no scope flag) installs into the Codewhale\n **global** owned root.\n- `/skills suggest` only reads the curated registry through the existing\n network policy. It never downloads, trusts, enables, or activates a skill;\n each result gives a separate `/skill install <name>` command for the user to\n choose.\n- If the same name exists in both project and global owned roots, update /\n uninstall / trust require `--project` or `--global`.\n- If a name exists only under a compatible external root, writes are refused;\n import it througLine truncated
"sha256": "d465564774757a3c4eeb7b266e5b50884f0a47365683f4b8b151b41466fdec9d",
"content": "# Skills Manager\n\nIn the terminal, `/skills` opens **Extensions → Skills**. Enter on a skill opens the dedicated manager, also available directly through `/skills manage`, for existing install, update, remove, and trust operations. `/skill <name>` still activates a skill, and the explicit inspection, remote, sync, and suggestion subcommands remain available.\n\n\n> 阅读简体中文版:[zh_hans/SKILLS.md](zh_hans/SKILLS.md)\n\nSkills are reusable `SKILL.md` instruction packs. Codewhale discovers them from\nseveral roots, but **only Codewhale-owned directories are writable**. The unified\n`/skills manage` manager is the interactive surface for audit and mutation; slash\naliases share the same write path.\n\nFor Claude Code plugin boundaries, see [CLAUDE_PLUGIN_COMPAT.md](CLAUDE_PLUGIN_COMPAT.md).\nFor `skills_dir` and `[skills]` config keys, see [CONFIGURATION.md](CONFIGURATION.md).\n\n## Architecture (four layers)\n\n| Layer | Role |\n| --- | --- |\n| **Root catalog** | Single source of precedence and ownership (`SkillRootCatalog`). |\n| **Audit** | Read-only, unmerged on-disk inventory (status, digest, actions). |\n| **Mutation controller** | Only writer for install / import / update / remove / trust. |\n| **Skills manager view** | TUI: emits events only; never writes files itself. |\n\nRuntime discovery (`SkillRegistry`) still merges skills for the model. Audit\nintentionally does **not** merge — it shows every on-disk copy so conflicts and\nshadowing stay visible.\n\n## Ownership and roots\n\n**Writable (Codewhale-owned)**\n\n| Scope | Path |\n| --- | --- |\n| Project | `<workspace>/.codewhale/skills/` |\n| Global | `~/.codewhale/skills/` |\n\n**Read-only compatible** (discover / import source only — never mutated in place)\n\nExamples: `<workspace>/.agents/skills`, `./skills`, `.claude/skills`,\n`.cursor/skills`, `.opencode/skills`, `~/.agents/skills`, `~/.claude/skills`,\nand similar harness layouts.\n\n**Audit-only (not runtime-active)**\n\n- Flat `<workspace>/skills` is an audit candidate until `[skills] flat_workspace_root = true`; an explicit `skills_dir` can also select it.\n- `.codex/skills` appears in **compatible** audit scans so operators can see it.\n It does **not** join the runtime discovery set.\n\nConfigured `skills_dir` that is not one of the owned Codewhale roots stays\nread-only. Discovery and the manager can list it; mutations still target owned\nproject/global roots only.\n\nWithin each scope, the owned `.codewhale/skills` root wins name collisions.\nProject order is `.codewhale`, `.agents`, `.claude`, `.opencode`, `.cursor`, then\nan opted-in flat `skills` root. Global order is `.codewhale`, `.agents`, `.claude`,\nthen legacy `.deepseek`. Project roots precede global roots. Shadowing warnings\nname both copies, so install and update select the owned copy in that scope.\n\nEvery root inside the workspace — owned, compatible, or a configured\n`skills_dir` that resolves there — loads only once the workspace is trusted\n(`/trust on --save`). Until then discovery names the skipped directories in its\nwarning, and the session's skills directory falls back to the global one.\n\n## Slash commands\n\n| Command | Behavior |\n| --- | --- |\n| `/skills` | Opens Extensions → Skills (owned-only scan, **no network**). |\n| `/skills manage` | Opens the dedicated Skills Manager for audit and mutation. |\n| `/skills <prefix>` | Text list filtered by name prefix. |\n| `/skills inspect` | Text discovery mode, searched directories, and source paths. |\n| `/skills --remote` | Explicit registry listing (network). |\n| `/skills suggest <task>` | Rank up to three remote skills for a task, with matching evidence and an explicit install command (network; no install). |\n| `/skills sync` | Explicit registry → local cache sync (network). |\n| `/skill <name>` | Activate a skill for the next turn. |\n| `/skill install [--project\\|--global] <spec>` | Install via mutation controller. |\n| `/skill update [--project\\|--global] <name>` | Update a managed skill from its registry provenance. |\n| `/skill uninstall [--project\\|--global] <name>` | Remove a managed skill. |\n| `/skill trust [--project\\|--global] <name>` | Write digest-bound advisory trust. |\n\nNotes:\n\n- There is **no** `/skills audit` subcommand. Use the manager (and `c` to toggle\n compatible roots) or `/skills inspect` for discovery details.\n- Bare `/skill install <spec>` (no scope flag) installs into the Codewhale\n **global** owned root.\n- `/skills suggest` only reads the curated registry through the existing\n network policy. It never downloads, trusts, enables, or activates a skill;\n each result gives a separate `/skill install <name>` command for the user to\n choose.\n- If the same name exists in both project and global owned roots, update /\n uninstall / trust require `--project` or `--global`.\n- If a name exists only under a compatible external root, writes are refused;\n import it througLine truncated
},
{
"path": "docs/CLAUDE_PLUGIN_COMPAT.md",
@@ -256,8 +256,8 @@
},
{
"path": "docs/CONFIGURATION.md",
"sha256": "8497b51cee60446c7532b7cb00ecfb475a6db2f2c63ff05513ba0bda626f523e",
"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": "e98c48c12a1706233342bb483aea239dabce283fc3851adf10ea0038fd8f93b6",
"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
}
]
},
@@ -336,8 +336,8 @@
},
{
"path": "docs/CONTRIBUTORS.md",
"sha256": "f4a6a5e2f62b5ca09d096eb4b2d8a927663573c4986b523afed81300e466e74e",
"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": "f7d902e50e5fda28bf6b42dac8bb33dc4b6a47e295a7902cda04f0cce76d1960",
"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",
@@ -384,8 +384,8 @@
"sources": [
{
"path": "docs/TOOL_SURFACE.md",
"sha256": "419bb600a25de7ec3931e139fa2019d899eb1513413ae726b737f0629d6ba7ba",
"content": "# Tool surface\n\n> 阅读简体中文版:[zh_hans/TOOL_SURFACE.md](zh_hans/TOOL_SURFACE.md)。\n\nThis document describes the current model-facing tool contract. The v0.9.1\ncutover that produced it is recorded in `docs/RUNTIME_SIMPLIFICATION_DESIGN.md`;\nread the workspace version from `Cargo.toml`, not from this line. The registry\nremains larger than the first-turn catalog so\nsaved transcripts can replay and uncommon capabilities can be loaded on demand.\nThe model should learn one canonical name for each common operation.\n\nImplementation sources:\n\n- `crates/engine/src/core/engine/tool_catalog.rs` owns the eager/deferred catalog.\n- `crates/engine/src/tools/registry.rs` registers canonical tools and hidden aliases.\n- `crates/engine/src/tools/{file,file_tool,shell}.rs` own the small foreground\n primitive behavior and schemas; the other native tools remain searchable.\n- `docs/RUNTIME_SIMPLIFICATION_DESIGN.md` records the v0.9.1 cutover and receipt.\n\n## Default-active contract\n\nNew turns start with fifteen eager native names plus synthetic `tool_search`:\n\n1. `read`\n2. `write`\n3. `edit`\n4. `file_search`\n5. `bash`\n6. `agent`\n7. `workflow`\n8. `todo_write`\n9. `create_goal`\n10. `get_goal`\n11. `update_goal`\n12. `task_shell_wait`\n13. `load_skill`\n14. `context_archive_get`\n15. `context_archive_search`\n16. `tool_search` (synthetic, always active)\n\nThe fifteen native names are `DEFAULT_ACTIVE_NATIVE_TOOLS` in\n`crates/engine/src/core/engine/tool_catalog.rs`, pinned by\n`default_active_contract_keeps_discovery_and_core_tools_eager`. An authority\nboundary may remove `agent` at the maximum child depth, and Plan registers only\nthe foreground shell surface (so no `task_shell_wait`), but route size alone\nmust not change this core vocabulary.\n\nThe direct schemas deliberately stay small:\n\n| Tool | Input | Purpose |\n|---|---|---|\n| `read` | `path`, optional `offset`, optional `limit` | Read a bounded file window with explicit continuation or truncation notices. |\n| `write` | `path`, `content` | Create or replace a file. |\n| `edit` | `path`, `edits` | Apply one or more unambiguous text replacements against one original snapshot. |\n| `file_search` | `query`, optional `path`, `limit`, `extensions`, `exclude` | Find files by name, path fragment or glob and return bounded, ranked matches. Respects ignore files; `exclude` replaces default exclusions. |\n| `bash` | `command`, optional `timeout`, optional `description` | Run one cancellable foreground shell command and return a bounded tail. The optional description is display-only. |\n| `agent` | delegated task and optional scope/context controls | Start or inspect focused child work. |\n| `workflow` | plan/script/source_path plus run controls | Coordinate multi-agent phases with dependencies and completion checks. |\n| `todo_write` | complete replacement list of `{content, status}` items | Keep optional, agent-owned progress notes for genuinely multi-step work. |\n| `create_goal` | objective plus optional budget | Start the session goal the turn works toward. |\n| `get_goal` | none | Read the active goal and its progress. |\n| `update_goal` | terminal status | Mark the goal complete or blocked. |\n| `task_shell_wait` | `task_id`, optional `wait`, `timeout_ms`, `cancel`, `gate` | Poll or stop a background shell task. Completion still arrives as a runtime event, so use it for early output, true barriers, or gate evidence. |\n| `load_skill` | optional `name`, `query` | Load a named skill's `SKILL.md` body and companion-file list; omit `name` or use `name=\"list\"` to list eligible skills, or use `query` to search names and descriptions. |\n| `tool_search` | `query`, optional matching controls | Discover policy-allowed deferred tools and add selected schemas to this conversation's toolbox. |\n\nMode is an authority decision, not a synonym system. Plan, Work, and Operate\nuse the same primitive identities. Plan centrally refuses `write`, `edit`, and\n`bash`; Work and Operate still pass those calls through approval, sandbox,\ntrusted-path, repository-law, and managed-policy gates. Full Access changes\nordinary approval behavior but does not bypass hard safety or repository law.\n\n`update_plan` remains registered only for saved-artifact compatibility and is\nnot model-visible. `tasks`, `Git`, `Run`, `Web`, `remember`, and other\nspecialized capabilities are searchable rather than first-turn ceremony.\n\nOn the TUI's default (calm) shell card the Bash description is the headline,\nshown as written apart from stripped terminal escapes, collapsed whitespace and\na length bound, and the command sits on its own `$` row directly under it in\nevery state and at every width, including automatic runs. A card without a\ndescription leads with the `$` command instead. The labeled card\n(`show_tool_details = true` with `calm_mode = false`) keeps the command in its\nheader and the description as a muted `note:` row. The description nLine truncated
"sha256": "915fd375ed479c107ee46a52a8ebb941b5aec4f39473a1c297d5344de697bce7",
"content": "# Tool surface\n\n> 阅读简体中文版:[zh_hans/TOOL_SURFACE.md](zh_hans/TOOL_SURFACE.md)。\n\nThis document describes the current model-facing tool contract. The v0.9.1\ncutover that produced it is recorded in `docs/RUNTIME_SIMPLIFICATION_DESIGN.md`;\nread the workspace version from `Cargo.toml`, not from this line. The registry\nremains larger than the first-turn catalog so\nsaved transcripts can replay and uncommon capabilities can be loaded on demand.\nThe model should learn one canonical name for each common operation.\n\nImplementation sources:\n\n- `crates/engine/src/core/engine/tool_catalog.rs` owns the eager/deferred catalog.\n- `crates/engine/src/tools/registry.rs` registers canonical tools and hidden aliases.\n- `crates/engine/src/tools/{file,file_tool,shell}.rs` own the small foreground\n primitive behavior and schemas; the other native tools remain searchable.\n- `docs/RUNTIME_SIMPLIFICATION_DESIGN.md` records the v0.9.1 cutover and receipt.\n\n## Default-active contract\n\nNew turns start with fifteen eager native names plus synthetic `tool_search`:\n\n1. `read`\n2. `write`\n3. `edit`\n4. `file_search`\n5. `bash`\n6. `agent`\n7. `workflow`\n8. `todo_write`\n9. `create_goal`\n10. `get_goal`\n11. `update_goal`\n12. `task_shell_wait`\n13. `load_skill`\n14. `context_archive_get`\n15. `context_archive_search`\n16. `tool_search` (synthetic, always active)\n\nThe fifteen native names are `DEFAULT_ACTIVE_NATIVE_TOOLS` in\n`crates/engine/src/core/engine/tool_catalog.rs`, pinned by\n`default_active_contract_keeps_discovery_and_core_tools_eager`. An authority\nboundary may remove `agent` at the maximum child depth, and Plan registers only\nthe foreground shell surface (so no `task_shell_wait`), but route size alone\nmust not change this core vocabulary.\n\nThe direct schemas deliberately stay small:\n\n| Tool | Input | Purpose |\n|---|---|---|\n| `read` | `path`, optional `offset`, optional `limit` | Read a bounded file window with explicit continuation or truncation notices. |\n| `write` | `path`, `content` | Create or replace a file. |\n| `edit` | `path`, `edits` | Apply one or more unambiguous text replacements against one original snapshot. |\n| `file_search` | `query`, optional `path`, `limit`, `extensions`, `exclude` | Find files by name, path fragment or glob and return bounded, ranked matches. Respects ignore files; `exclude` replaces default exclusions. |\n| `bash` | `command`, optional `timeout`, optional `description` | Run one cancellable foreground shell command and return a bounded tail. The optional description is display-only. |\n| `agent` | delegated task and optional scope/context controls | Start or inspect focused child work. |\n| `workflow` | plan/script/source_path plus run controls | Coordinate multi-agent phases with dependencies and completion checks. |\n| `todo_write` | complete replacement list of `{content, status}` items | Keep optional, agent-owned progress notes for genuinely multi-step work. |\n| `create_goal` | objective plus optional budget | Start the session goal the turn works toward. |\n| `get_goal` | none | Read the active goal and its progress. |\n| `update_goal` | terminal status | Mark the goal complete or blocked. |\n| `task_shell_wait` | `task_id`, optional `wait`, `timeout_ms`, `cancel`, `gate` | Poll or stop a background shell task. Completion still arrives as a runtime event, so use it for early output, true barriers, or gate evidence. |\n| `load_skill` | optional `name`, `query` | Load a named skill's `SKILL.md` body and companion-file list; omit `name` or use `name=\"list\"` to list eligible skills, or use `query` to search names and descriptions. |\n| `tool_search` | `query`, optional matching controls | Discover policy-allowed deferred tools and add selected schemas to this conversation's toolbox. |\n\nMode is an authority decision, not a synonym system. Plan, Work, and Operate\nuse the same primitive identities. Plan centrally refuses `write`, `edit`, and\n`bash`; Work and Operate still pass those calls through approval, sandbox,\ntrusted-path, repository-law, and managed-policy gates. Full Access changes\nordinary approval behavior but does not bypass hard safety or repository law.\n\n`update_plan` remains registered only for saved-artifact compatibility and is\nnot model-visible. `tasks`, `Git`, `Run`, `Web`, `remember`, and other\nspecialized capabilities are searchable rather than first-turn ceremony.\n\nOn the TUI's default (calm) shell card the Bash description is the headline,\nshown as written apart from stripped terminal escapes, collapsed whitespace and\na length bound, and the command sits on its own `$` row directly under it in\nevery state and at every width, including automatic runs. A card without a\ndescription leads with the `$` command instead. The labeled card\n(`show_tool_details = true` with `calm_mode = false`) keeps the command in its\nheader and the description as a muted `note:` row. The description nLine truncated
},
{
"path": "docs/RUNTIME_SIMPLIFICATION_DESIGN.md",