Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 100 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "jcode"
version = "0.75.0"
version = "0.75.2"
description = "Possibly the greatest coding agent ever built — blazing-fast TUI, multi-model, swarm coordination, 30+ tools"
edition = "2024"
autobins = false
Expand Down Expand Up @@ -65,6 +65,7 @@ members = [
"crates/jcode-provider-openrouter-runtime",
"crates/jcode-provider-anthropic-runtime",
"crates/jcode-provider-openai-runtime",
"crates/jcode-provider-grok-build-runtime",
"crates/jcode-provider-doctor",
"crates/jcode-tui-markdown",
"crates/jcode-tui-messages",
Expand Down Expand Up @@ -200,6 +201,7 @@ jcode-provider-claude-cli-runtime = { path = "crates/jcode-provider-claude-cli-r
jcode-provider-openrouter-runtime = { path = "crates/jcode-provider-openrouter-runtime" }
jcode-provider-anthropic-runtime = { path = "crates/jcode-provider-anthropic-runtime" }
jcode-provider-openai-runtime = { path = "crates/jcode-provider-openai-runtime" }
jcode-provider-grok-build-runtime = { path = "crates/jcode-provider-grok-build-runtime" }
jcode-selfdev-types = { path = "crates/jcode-selfdev-types" }

# Archive extraction (for auto-update)
Expand Down
8 changes: 8 additions & 0 deletions changelog/index.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
{
"entries": [
{
"version": "0.75.2",
"date": "2026-08-11"
},
{
"version": "0.75.1",
"date": "2026-08-11"
},
{
"version": "0.75.0",
"date": "2026-08-10"
Expand Down
18 changes: 18 additions & 0 deletions changelog/v0.75.1.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"version": "0.75.1",
"date": "2026-08-11",
"title": "More reliable autonomous runs",
"highlights": [
"Todo completion synonyms such as done and finished no longer trigger false auto-poke loops",
"Grok Build is available as an ACP provider"
],
"improvements": [
"Todo status values are documented and constrained in the tool schema",
"Desktop and SDK clients expose provider request lifecycle status"
],
"fixes": [
"Todo statuses are normalized on write and compared case-insensitively during headless run completion checks",
"Codex quota windows no longer appear more than once",
"Pinned todo configuration tests no longer leak process-global configuration"
]
}
14 changes: 14 additions & 0 deletions changelog/v0.75.2.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"version": "0.75.2",
"date": "2026-08-11",
"title": "Strict todo status validation",
"highlights": [
"The todo tool now rejects unknown status values instead of storing them silently"
],
"improvements": [
"Invalid status errors list the accepted pending, in_progress, completed, and cancelled values"
],
"fixes": [
"Unknown model-written status strings can no longer leave todo completion behavior ambiguous"
]
}
65 changes: 60 additions & 5 deletions crates/jcode-app-core/src/tool/todo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use crate::todo::{
feedback_loop_passes, intent_understanding_passes, load_goals, load_plan, load_todos,
save_goals, save_plan, save_todos, update_todo_review_cycle,
};
use anyhow::Result;
use anyhow::{Result, bail};
use async_trait::async_trait;
use serde::Deserialize;
use serde_json::{Value, json};
Expand Down Expand Up @@ -72,6 +72,21 @@ struct TodoInput {
plan: Option<TodoPlan>,
}

fn parse_todo_input(input: Value) -> Result<TodoInput> {
let params: TodoInput = serde_json::from_value(normalize_todo_input(input))?;
if let Some(todo) = params.todos.as_ref().and_then(|todos| {
todos
.iter()
.find(|todo| crate::todo::canonical_todo_status(&todo.status).is_none())
}) {
bail!(
"invalid todo status {:?}; expected one of: pending, in_progress, completed, cancelled",
todo.status
);
}
Ok(params)
}

/// Normalize a goal's group label: trimmed, with empty/whitespace collapsed
/// to `None` (the implicit goal of an ungrouped list).
fn goal_group_key(group: Option<&str>) -> Option<String> {
Expand Down Expand Up @@ -656,6 +671,12 @@ fn normalize_todo_input(mut input: Value) -> Value {
let Some(fields) = item.as_object_mut() else {
continue;
};
if key == "todos"
&& let Some(Value::String(status)) = fields.get_mut("status")
&& let Some(canonical) = crate::todo::canonical_todo_status(status)
{
*status = canonical.to_string();
}
for key in [
"confidence",
"completion_confidence",
Expand Down Expand Up @@ -727,7 +748,8 @@ impl Tool for TodoTool {
},
"status": {
"type": "string",
"description": "Status."
"enum": ["pending", "in_progress", "completed", "cancelled"],
"description": "Status. Use completed when the task is done."
},
"priority": {
"type": "string",
Expand Down Expand Up @@ -837,7 +859,7 @@ impl Tool for TodoTool {
}

async fn execute(&self, input: Value, ctx: ToolContext) -> Result<ToolOutput> {
let params: TodoInput = serde_json::from_value(normalize_todo_input(input))?;
let params = parse_todo_input(input)?;
let is_write = params.todos.is_some() || params.goals.is_some() || params.plan.is_some();
let operation = if is_write { "write" } else { "read" };
let result = if is_write {
Expand Down Expand Up @@ -1178,8 +1200,8 @@ mod tests {
}
}

fn parse(input: Value) -> Result<TodoInput, serde_json::Error> {
serde_json::from_value(normalize_todo_input(input))
fn parse(input: Value) -> Result<TodoInput> {
parse_todo_input(input)
}

#[test]
Expand Down Expand Up @@ -1222,6 +1244,39 @@ mod tests {
);
}

#[test]
fn normalizes_natural_and_case_varied_todo_statuses() {
let parsed = parse(json!({
"todos": [
{"content": "a", "status": "done", "priority": "high", "id": "1", "confidence": "verified"},
{"content": "b", "status": " Finished ", "priority": "low", "id": "2", "confidence": "validated"},
{"content": "c", "status": "Canceled", "priority": "low", "id": "3", "confidence": "plausible"}
]
}))
.expect("status synonyms should parse");
let statuses: Vec<_> = parsed
.todos
.expect("todos present")
.into_iter()
.map(|todo| todo.status)
.collect();
assert_eq!(statuses, ["completed", "completed", "cancelled"]);
}

#[test]
fn rejects_unknown_todo_statuses_with_valid_vocabulary() {
let error = parse(json!({
"todos": [
{"content": "a", "status": "blocked", "priority": "high", "id": "1", "confidence": "plausible"}
]
}))
.err()
.expect("unknown status should be rejected");
let message = error.to_string();
assert!(message.contains("invalid todo status \"blocked\""));
assert!(message.contains("pending, in_progress, completed, cancelled"));
}

#[test]
fn accepts_float_confidence_and_empty_string_as_none() {
let input = json!({
Expand Down
16 changes: 16 additions & 0 deletions crates/jcode-base/src/auth/grok_build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
//! Local Grok CLI discovery for the delegated Grok Build provider.

use std::path::PathBuf;

pub const CLI_PATH_ENV: &str = "JCODE_GROK_CLI_PATH";

pub fn cli_path() -> PathBuf {
std::env::var_os(CLI_PATH_ENV)
.filter(|value| !value.is_empty())
.map(PathBuf::from)
.unwrap_or_else(|| PathBuf::from("grok"))
}

pub fn cli_available() -> bool {
super::command_exists(cli_path().to_string_lossy().as_ref())
}
1 change: 1 addition & 0 deletions crates/jcode-base/src/auth/integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ pub fn runtime_id_for_login_provider(
LoginProviderTarget::Azure => Some(RuntimeProviderId::AzureOpenAi),
LoginProviderTarget::OpenAiCompatible(_) => Some(RuntimeProviderId::OpenAiCompatible),
LoginProviderTarget::Cursor => Some(RuntimeProviderId::Cursor),
LoginProviderTarget::GrokBuild => Some(RuntimeProviderId::GrokBuild),
LoginProviderTarget::Copilot => Some(RuntimeProviderId::Copilot),
LoginProviderTarget::Gemini => Some(RuntimeProviderId::Gemini),
LoginProviderTarget::Antigravity => Some(RuntimeProviderId::Antigravity),
Expand Down
Loading
Loading