From 40a61b3691bf64c23fb677a5665bb17cf198e3e9 Mon Sep 17 00:00:00 2001 From: jeremy <94247773+1jehuang@users.noreply.github.com> Date: Thu, 6 Aug 2026 00:00:09 -0700 Subject: [PATCH 01/45] fix: update Claude memory sidecar model (fixes #798) --- crates/jcode-base/src/sidecar.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/jcode-base/src/sidecar.rs b/crates/jcode-base/src/sidecar.rs index f8973f466a..45936bf395 100644 --- a/crates/jcode-base/src/sidecar.rs +++ b/crates/jcode-base/src/sidecar.rs @@ -19,7 +19,7 @@ const SIDECAR_OPENAI_OAUTH_FALLBACK_MODEL: &str = "gpt-5.4"; const SIDECAR_OPENAI_OAUTH_FALLBACK_REASONING: &str = "low"; /// Fast/cheap Claude model used when only Claude credentials are available. -const SIDECAR_CLAUDE_MODEL: &str = "claude-haiku-4-5-20241022"; +const SIDECAR_CLAUDE_MODEL: &str = "claude-haiku-4-5-20251001"; /// OpenAI Responses API const OPENAI_API_BASE: &str = "https://api.openai.com/v1"; @@ -1037,6 +1037,7 @@ mod tests { #[test] fn test_sidecar_fast_model() { assert_eq!(SIDECAR_FAST_MODEL, "gpt-5.6-luna"); + assert_eq!(SIDECAR_CLAUDE_MODEL, "claude-haiku-4-5-20251001"); } #[test] From 93139fe019ded62a738cf5f9711bbcd40ef0aba1 Mon Sep 17 00:00:00 2001 From: jeremy <94247773+1jehuang@users.noreply.github.com> Date: Thu, 6 Aug 2026 00:00:09 -0700 Subject: [PATCH 02/45] fix: wait for daemon registration during SDK close (fixes #818) --- sdk/typescript/src/launch.ts | 28 +++++++++++++++++++++++++++- sdk/typescript/test/launch.test.ts | 18 ++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/sdk/typescript/src/launch.ts b/sdk/typescript/src/launch.ts index 27b24bcad6..222fe462c2 100644 --- a/sdk/typescript/src/launch.ts +++ b/sdk/typescript/src/launch.ts @@ -349,6 +349,29 @@ function readDaemonPidSync(jcodeHome: string, runtimeDir: string): number | unde return undefined; } +/** Wait briefly for a newly started daemon to publish its registry entry. */ +async function waitForDaemonPid( + jcodeHome: string, + runtimeDir: string, + timeoutMs = 2000, +): Promise { + const deadline = Date.now() + timeoutMs; + do { + const pid = readDaemonPidSync(jcodeHome, runtimeDir); + if (pid !== undefined) return pid; + await new Promise((resolve) => setTimeout(resolve, 50)); + } while (Date.now() < deadline); + return undefined; +} + +export async function waitForDaemonPidForTest( + jcodeHome: string, + runtimeDir: string, + timeoutMs?: number, +): Promise { + return waitForDaemonPid(jcodeHome, runtimeDir, timeoutMs); +} + /** * Stop an instance's daemon and wait for it to actually be gone. * @@ -363,7 +386,10 @@ async function stopInstanceDaemon( jcodeHome: string, runtimeDir: string, ): Promise { - const pid = readDaemonPidSync(jcodeHome, runtimeDir); + // The API socket can become connectable just before the daemon writes its + // servers.json entry. close() may therefore run during this small startup + // window, so do not silently give up after a single registry read. + const pid = await waitForDaemonPid(jcodeHome, runtimeDir); if (pid === undefined) return; const signal = (sig: NodeJS.Signals) => { diff --git a/sdk/typescript/test/launch.test.ts b/sdk/typescript/test/launch.test.ts index 0f5f162b80..8a0a213149 100644 --- a/sdk/typescript/test/launch.test.ts +++ b/sdk/typescript/test/launch.test.ts @@ -61,6 +61,24 @@ test("cleanup refuses arbitrary directories even when asked directly", async () fs.rmSync(arbitrary, { recursive: true, force: true }); }); +test("daemon shutdown lookup waits for a delayed registry entry", async () => { + const home = fs.mkdtempSync(path.join(os.tmpdir(), "jcode-daemon-race-test-")); + const runtimeDir = path.join(home, "run"); + fs.mkdirSync(runtimeDir); + const { waitForDaemonPidForTest } = await import("../dist/launch.js"); + + const lookup = waitForDaemonPidForTest(home, runtimeDir, 1000); + setTimeout(() => { + fs.writeFileSync( + path.join(home, "servers.json"), + JSON.stringify({ instance: { socket: path.join(runtimeDir, "jcode.sock"), pid: 4242 } }), + ); + }, 100); + + assert.equal(await lookup, 4242); + fs.rmSync(home, { recursive: true, force: true }); +}); + /** Inheriting must share rotating credentials, not copy them. */ test("rotating credentials are shared so token refresh stays coherent", () => { const sandbox = fs.mkdtempSync(path.join(os.tmpdir(), "jcode-inherit-test-")); From 742973c20959f1020aa5b194436c51d46f9b5a22 Mon Sep 17 00:00:00 2001 From: jeremy <94247773+1jehuang@users.noreply.github.com> Date: Thu, 6 Aug 2026 00:12:41 -0700 Subject: [PATCH 03/45] style: satisfy current clippy guardrail --- crates/jcode-provider-core/src/openai_schema.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/jcode-provider-core/src/openai_schema.rs b/crates/jcode-provider-core/src/openai_schema.rs index 9dc7919a27..3d5db28fac 100644 --- a/crates/jcode-provider-core/src/openai_schema.rs +++ b/crates/jcode-provider-core/src/openai_schema.rs @@ -117,10 +117,10 @@ pub fn schema_supports_strict(schema: &Value) -> bool { // `description`) is valid JSON Schema, but strict normalization turns it // into an untyped `anyOf` branch that makes OpenAI reject the entire tool // catalog. Fall back to non-strict instead. See issue #713. - if let Some(Value::Object(props)) = map.get("properties") { - if props.values().any(|prop| !schema_has_type_info(prop)) { - return false; - } + if let Some(Value::Object(props)) = map.get("properties") + && props.values().any(|prop| !schema_has_type_info(prop)) + { + return false; } map.values().all(schema_supports_strict) From 6523b9b6ab18abf3eb179b97df9147a281dc6664 Mon Sep 17 00:00:00 2001 From: jeremy <94247773+1jehuang@users.noreply.github.com> Date: Thu, 6 Aug 2026 00:20:41 -0700 Subject: [PATCH 04/45] style: remove obsolete auth token helper --- crates/jcode-base/src/auth/mod.rs | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/crates/jcode-base/src/auth/mod.rs b/crates/jcode-base/src/auth/mod.rs index 179b578208..bf1f917b2c 100644 --- a/crates/jcode-base/src/auth/mod.rs +++ b/crates/jcode-base/src/auth/mod.rs @@ -986,19 +986,6 @@ fn record_auth_probe_step( timings.push((name, step_start.elapsed().as_millis())); } -fn token_state(result: anyhow::Result) -> AuthState { - match result { - Ok(is_expired) => { - if is_expired { - AuthState::Expired - } else { - AuthState::Available - } - } - Err(_) => AuthState::NotConfigured, - } -} - /// Auth state for an OAuth credential that refreshes automatically. /// /// A short-lived access token is *not* a broken login. Antigravity/Gemini From 38721c5001532183ce8bd33aee59fb230774d8bc Mon Sep 17 00:00:00 2001 From: jeremy <94247773+1jehuang@users.noreply.github.com> Date: Thu, 6 Aug 2026 00:21:30 -0700 Subject: [PATCH 05/45] test: make strict schema fixture fully typed --- crates/jcode-provider-core/src/openai_schema.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/crates/jcode-provider-core/src/openai_schema.rs b/crates/jcode-provider-core/src/openai_schema.rs index 3d5db28fac..5848fd5d0d 100644 --- a/crates/jcode-provider-core/src/openai_schema.rs +++ b/crates/jcode-provider-core/src/openai_schema.rs @@ -551,7 +551,6 @@ mod tests { }); let normalized = openai_compatible_schema(&schema); - assert!(normalized.get("allOf").is_none()); assert_eq!(normalized["type"], json!("object")); assert_eq!(normalized["description"], json!("Read params")); @@ -660,13 +659,12 @@ mod tests { "properties": { "path": { "type": "string", "description": "where" }, "count": { "type": "integer" }, - "mode": { "enum": ["fast", "slow"] }, + "mode": { "type": "string", "enum": ["fast", "slow"] }, "nested": { "type": "object", "properties": { "inner": { "type": "boolean" } } }, - "either": { "anyOf": [{ "type": "string" }, { "type": "integer" }] }, - "anything": true + "either": { "anyOf": [{ "type": "string" }, { "type": "integer" }] } }, "required": ["path"] }); From 951c962dff682a26db16815acd6df3c289e1c009 Mon Sep 17 00:00:00 2001 From: jeremy <94247773+1jehuang@users.noreply.github.com> Date: Thu, 6 Aug 2026 00:28:44 -0700 Subject: [PATCH 06/45] style: satisfy Rust 1.97 app-core lints --- crates/jcode-app-core/src/notifications.rs | 2 +- crates/jcode-app-core/src/server/client_lifecycle.rs | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/jcode-app-core/src/notifications.rs b/crates/jcode-app-core/src/notifications.rs index 99b1812ba4..eaecf74c95 100644 --- a/crates/jcode-app-core/src/notifications.rs +++ b/crates/jcode-app-core/src/notifications.rs @@ -452,7 +452,7 @@ pub fn send_macos_turn_notification( #[cfg(not(target_os = "macos"))] { let _ = (title, subtitle, body, sound); - return false; + false } #[cfg(target_os = "macos")] diff --git a/crates/jcode-app-core/src/server/client_lifecycle.rs b/crates/jcode-app-core/src/server/client_lifecycle.rs index 8278c938c1..e3b3401939 100644 --- a/crates/jcode-app-core/src/server/client_lifecycle.rs +++ b/crates/jcode-app-core/src/server/client_lifecycle.rs @@ -2834,6 +2834,7 @@ async fn append_context_message( let _ = client_event_tx.send(event); } +#[allow(clippy::too_many_arguments)] async fn start_processing_message( message: ProcessingMessage, client_session_id: &str, From b0f4c2e513981fc42c5c8159ef97321e898cac06 Mon Sep 17 00:00:00 2001 From: jeremy <94247773+1jehuang@users.noreply.github.com> Date: Thu, 6 Aug 2026 01:26:37 -0700 Subject: [PATCH 07/45] Improve todo quality gate rendering --- crates/jcode-tui/src/tui/ui_messages.rs | 155 +++++++++++++++--- crates/jcode-tui/src/tui/ui_messages/tests.rs | 77 +++++++-- 2 files changed, 193 insertions(+), 39 deletions(-) diff --git a/crates/jcode-tui/src/tui/ui_messages.rs b/crates/jcode-tui/src/tui/ui_messages.rs index 10b6932fb2..3313f80ea3 100644 --- a/crates/jcode-tui/src/tui/ui_messages.rs +++ b/crates/jcode-tui/src/tui/ui_messages.rs @@ -944,6 +944,14 @@ fn todo_score_color() -> Color { rgb(105, 205, 165) } +fn todo_warning_color() -> Color { + rgb(225, 180, 80) +} + +fn todo_failure_color() -> Color { + rgb(225, 105, 105) +} + fn todo_confidence_color() -> Color { rgb(135, 155, 180) } @@ -1207,33 +1215,91 @@ fn todo_card_goal_for_group<'a>( }) } -fn todo_goal_score_spans(goal: Option<&crate::todo::TodoGoal>) -> Vec> { - let Some(goal) = goal else { - return Vec::new(); - }; +fn todo_goal_score_spans(goal: &crate::todo::TodoGoal) -> Vec> { let mut spans = Vec::new(); - let mut states: Vec<(&str, String)> = Vec::new(); - if let Some(state) = goal.closed_feedback_loop { - states.push(("Closed feedback loop", state.as_str().to_string())); + let mut states: Vec<(&str, String, Color)> = Vec::new(); + if !crate::todo::feedback_loop_passes(goal.closed_feedback_loop) { + let (state, color) = goal.closed_feedback_loop.map_or_else( + || ("missing".to_string(), todo_failure_color()), + |state| { + let color = if state <= crate::todo::FeedbackLoopState::Weak { + todo_failure_color() + } else { + todo_warning_color() + }; + (state.as_str().to_string(), color) + }, + ); + states.push(("Closed feedback loop", state, color)); + } + if !crate::todo::feedback_loop_relevance_passes(goal) { + let (state, color) = goal.feedback_loop_relevance.map_or_else( + || ("missing".to_string(), todo_failure_color()), + |state| { + let color = if state == crate::todo::FeedbackLoopRelevance::Indirect { + todo_failure_color() + } else { + todo_warning_color() + }; + (state.as_str().to_string(), color) + }, + ); + states.push(("Relevance", state, color)); + } + if !crate::todo::feedback_loop_coverage_passes(goal) { + let (state, color) = goal.feedback_loop_coverage.map_or_else( + || ("missing".to_string(), todo_failure_color()), + |state| { + let color = if state == crate::todo::FeedbackLoopCoverage::Narrow { + todo_failure_color() + } else { + todo_warning_color() + }; + (state.as_str().to_string(), color) + }, + ); + states.push(("Coverage", state, color)); } - if let Some(state) = goal.feedback_loop_relevance { - states.push(("Relevance", state.as_str().to_string())); + + if states.is_empty() { + spans.push(Span::styled( + "✓ All quality gates passing", + Style::default().fg(todo_score_color()), + )); } - if let Some(state) = goal.feedback_loop_coverage { - states.push(("Coverage", state.as_str().to_string())); + + for (index, (label, state, color)) in states.into_iter().enumerate() { + if index > 0 { + spans.push(Span::styled(" · ", Style::default().fg(dim_color()))); + } + spans.push(Span::styled( + format!("{} ", label), + Style::default().fg(todo_label_color()), + )); + spans.push(Span::styled(state, Style::default().fg(color))); } + + // Delivery is progress toward the outcome, not a quality gate. Keep it + // visible and visually separate from failures so it cannot read as one. if let Some(state) = goal.delivery_state { - states.push(("Delivery", state.as_str().to_string())); - } - for (label, state) in states { if !spans.is_empty() { spans.push(Span::styled(" · ", Style::default().fg(dim_color()))); } spans.push(Span::styled( - format!("{} ", label), + "Delivery ", Style::default().fg(todo_label_color()), )); - spans.push(Span::styled(state, Style::default().fg(todo_score_color()))); + let color = if state >= crate::todo::DeliveryState::WorkflowValidated { + todo_score_color() + } else if state == crate::todo::DeliveryState::Integrated { + todo_warning_color() + } else { + todo_failure_color() + }; + spans.push(Span::styled( + state.as_str().to_string(), + Style::default().fg(color), + )); } spans } @@ -1463,23 +1529,42 @@ fn push_todo_goal_details( let Some(goal) = goal else { return; }; - let scores = todo_goal_score_spans(Some(goal)); + let scores = todo_goal_score_spans(goal); if !scores.is_empty() { let score_width = Line::from(scores.clone()).width(); - let score_count = usize::from(goal.closed_feedback_loop.is_some()) - + usize::from(goal.feedback_loop_relevance.is_some()) - + usize::from(goal.feedback_loop_coverage.is_some()) + let score_count = usize::from(!crate::todo::feedback_loop_passes( + goal.closed_feedback_loop, + )) + usize::from(!crate::todo::feedback_loop_relevance_passes(goal)) + + usize::from(!crate::todo::feedback_loop_coverage_passes(goal)) + usize::from(goal.delivery_state.is_some()); if score_width > inner_width.saturating_sub(2) && score_count > 1 { let mut states: Vec<(&str, String)> = Vec::new(); - if let Some(state) = goal.closed_feedback_loop { - states.push(("Closed feedback loop", state.as_str().to_string())); + if !crate::todo::feedback_loop_passes(goal.closed_feedback_loop) { + states.push(( + "Closed feedback loop", + goal.closed_feedback_loop + .map(|state| state.as_str()) + .unwrap_or("missing") + .to_string(), + )); } - if let Some(state) = goal.feedback_loop_relevance { - states.push(("Relevance", state.as_str().to_string())); + if !crate::todo::feedback_loop_relevance_passes(goal) { + states.push(( + "Relevance", + goal.feedback_loop_relevance + .map(|state| state.as_str()) + .unwrap_or("missing") + .to_string(), + )); } - if let Some(state) = goal.feedback_loop_coverage { - states.push(("Coverage", state.as_str().to_string())); + if !crate::todo::feedback_loop_coverage_passes(goal) { + states.push(( + "Coverage", + goal.feedback_loop_coverage + .map(|state| state.as_str()) + .unwrap_or("missing") + .to_string(), + )); } if let Some(state) = goal.delivery_state { states.push(("Delivery", state.as_str().to_string())); @@ -1490,7 +1575,23 @@ fn push_todo_goal_details( format!("{} ", label), Style::default().fg(todo_label_color()), )); - spans.push(Span::styled(state, Style::default().fg(todo_score_color()))); + let color = if label == "Delivery" { + match crate::todo::DeliveryState::parse(&state) { + Some(value) if value >= crate::todo::DeliveryState::WorkflowValidated => { + todo_score_color() + } + Some(crate::todo::DeliveryState::Integrated) => todo_warning_color(), + _ => todo_failure_color(), + } + } else if matches!( + state.as_str(), + "missing" | "absent" | "weak" | "indirect" | "narrow" + ) { + todo_failure_color() + } else { + todo_warning_color() + }; + spans.push(Span::styled(state, Style::default().fg(color))); lines.push(todo_card_line(spans, base_indent, inner_width)); } } else { diff --git a/crates/jcode-tui/src/tui/ui_messages/tests.rs b/crates/jcode-tui/src/tui/ui_messages/tests.rs index ab75e0eee8..0feb53d43a 100644 --- a/crates/jcode-tui/src/tui/ui_messages/tests.rs +++ b/crates/jcode-tui/src/tui/ui_messages/tests.rs @@ -583,14 +583,11 @@ fn render_todos_message_shows_goal_scores_without_verbose_feedback() { .collect::>() .join("\n"); - for assessment in [ - "Closed feedback loop strong", - "Relevance representative", - "Coverage main_paths", - "Delivery workflow_validated", - ] { + for assessment in ["Closed feedback loop strong", "Delivery workflow_validated"] { assert!(plain.contains(assessment), "{plain}"); } + assert!(!plain.contains("Relevance representative"), "{plain}"); + assert!(!plain.contains("Coverage main_paths"), "{plain}"); // Plan-level intent renders once, above the groups. assert!(plain.contains("Understands user intent clear"), "{plain}"); assert!( @@ -671,8 +668,15 @@ fn render_todos_message_compacts_long_details_at_narrow_widths() { .map(extract_line_text) .collect::>(); assert!( - wide.len() > narrow.len(), - "wide={wide:?}\nnarrow={narrow:?}" + wide.iter() + .any(|line| line.contains("narrow terminal window")), + "wide={wide:?}" + ); + assert!( + !narrow + .iter() + .any(|line| line.contains("narrow terminal window")), + "narrow={narrow:?}" ); } @@ -718,9 +722,58 @@ fn render_todos_message_uses_readable_semantic_colors() { assert_eq!(color_for("● "), Some(asap_color())); assert_eq!(color_for(" (high)"), None); assert_eq!(color_for(" · plausible"), Some(todo_confidence_color())); + assert_eq!(color_for("strong"), Some(todo_warning_color())); + assert_eq!(color_for("missing"), Some(todo_failure_color())); assert_ne!(todo_meta_color(), dim_color()); } +#[test] +fn render_todos_message_collapses_passing_quality_gates() { + let todos = vec![crate::todo::TodoItem { + id: "1".to_string(), + content: "Verify the result".to_string(), + status: "completed".to_string(), + priority: "high".to_string(), + group: Some("quality".to_string()), + confidence: None, + completion_confidence: Some(crate::todo::ConfidenceState::Validated), + confidence_history: Vec::new(), + blocked_by: Vec::new(), + assigned_to: None, + }]; + let goals = vec![crate::todo::TodoGoal { + group: Some("quality".to_string()), + closed_feedback_loop: Some(crate::todo::FeedbackLoopState::Closed), + feedback_loop_relevance: Some(crate::todo::FeedbackLoopRelevance::AcceptanceAligned), + feedback_loop_coverage: Some(crate::todo::FeedbackLoopCoverage::EdgeAndIntegrationPaths), + delivery_state: Some(crate::todo::DeliveryState::OutcomeDelivered), + ..Default::default() + }]; + let msg = + DisplayMessage::todos(serde_json::json!({ "todos": todos, "goals": goals }).to_string()); + let lines = render_todos_message(&msg, 100, crate::config::DiffDisplayMode::Off); + let plain = lines + .iter() + .map(extract_line_text) + .collect::>() + .join("\n"); + + assert!(plain.contains("✓ All quality gates passing"), "{plain}"); + assert!(plain.contains("Delivery outcome_delivered"), "{plain}"); + assert!(!plain.contains("Closed feedback loop closed"), "{plain}"); + assert!(!plain.contains("Relevance acceptance_aligned"), "{plain}"); + assert!( + !plain.contains("Coverage edge_and_integration_paths"), + "{plain}" + ); + let passing = lines + .iter() + .flat_map(|line| line.spans.iter()) + .find(|span| span.content.as_ref() == "✓ All quality gates passing") + .and_then(|span| span.style.fg); + assert_eq!(passing, Some(todo_score_color())); +} + #[test] fn render_todos_message_wraps_goal_scores_at_narrow_widths() { let todos = vec![crate::todo::TodoItem { @@ -832,10 +885,10 @@ fn render_todo_tool_result_uses_borderless_card_with_goal_scores() { assert!(!plain.contains("Todos"), "{plain}"); assert!(plain.contains("todo rendering ●"), "{plain}"); - assert!( - plain.contains("Closed feedback loop strong · Delivery workflow_validated"), - "{plain}" - ); + assert!(plain.contains("Closed feedback loop strong"), "{plain}"); + assert!(plain.contains("Relevance missing"), "{plain}"); + assert!(plain.contains("Coverage missing"), "{plain}"); + assert!(plain.contains("Delivery workflow_validated"), "{plain}"); assert!( plain.contains("● Render the todo result · plausible"), "{plain}" From 8d5a4ea9f7c0a27dabdfdbdd902ee115dccf29c1 Mon Sep 17 00:00:00 2001 From: jeremy <94247773+1jehuang@users.noreply.github.com> Date: Thu, 6 Aug 2026 01:31:44 -0700 Subject: [PATCH 08/45] feat(todo): distinguish synthetic validation --- crates/jcode-app-core/src/tool/todo.rs | 25 +++++++++++++++++++++++-- crates/jcode-base/src/todo.rs | 26 +++++++++++++++++++++++++- crates/jcode-task-types/src/lib.rs | 21 +++++++++++++++++++-- 3 files changed, 67 insertions(+), 5 deletions(-) diff --git a/crates/jcode-app-core/src/tool/todo.rs b/crates/jcode-app-core/src/tool/todo.rs index b01d785e5d..7f3e0aad56 100644 --- a/crates/jcode-app-core/src/tool/todo.rs +++ b/crates/jcode-app-core/src/tool/todo.rs @@ -767,8 +767,8 @@ impl Tool for TodoTool { }, "feedback_loop_relevance": { "type": "string", - "enum": ["indirect", "representative", "acceptance_aligned"], - "description": "How directly the checks represent observable acceptance behavior through public interfaces rather than an internal proxy." + "enum": ["indirect", "synthetic", "representative", "acceptance_blocked", "acceptance_aligned"], + "description": "How directly checks represent observable acceptance behavior. indirect = inspection or an internal proxy; synthetic = custom harnesses, stubs, mocks, copied sources, or synthetic fixtures; representative = real public interfaces but not the complete acceptance workflow; acceptance_blocked = the real acceptance workflow was attempted but an external constraint prevented a result; acceptance_aligned = the real project build, integration test, or end-user workflow passed. Substitute-only validation is never acceptance_aligned." }, "feedback_loop_coverage": { "type": "string", @@ -983,6 +983,27 @@ mod tests { assert!(!goal_props.contains_key("alignment_score")); assert!(!goal_props.contains_key("objective")); assert_eq!(goal_props.len(), 10); + assert_eq!( + goal_props["feedback_loop_relevance"]["enum"], + json!([ + "indirect", + "synthetic", + "representative", + "acceptance_blocked", + "acceptance_aligned" + ]) + ); + let relevance_description = goal_props["feedback_loop_relevance"]["description"] + .as_str() + .expect("feedback-loop relevance should explain every state"); + for required_concept in [ + "custom harnesses", + "real public interfaces", + "external constraint", + "Substitute-only validation is never acceptance_aligned", + ] { + assert!(relevance_description.contains(required_concept)); + } let goal_required = props["goals"]["items"]["required"] .as_array() diff --git a/crates/jcode-base/src/todo.rs b/crates/jcode-base/src/todo.rs index 5cdc5fedf9..d64fadd13e 100644 --- a/crates/jcode-base/src/todo.rs +++ b/crates/jcode-base/src/todo.rs @@ -376,7 +376,7 @@ pub fn build_gate_digest( .map(|group| format!(" for \"{}\"", group)) .unwrap_or_default(); format!( - "the checks{} did not directly represent how the result will be used or accepted. Exercise the public interfaces and integration boundaries, and report the behavior a user or downstream system would observe.", + "the checks{} did not directly represent how the result will be used or accepted. Exercise the real project's public interfaces, integration boundaries, or end-user acceptance path and report the observed behavior. A custom harness, stub, mock, copied source, or synthetic fixture is useful evidence but cannot replace that path; if the real path is externally blocked, record that constraint honestly.", label ) } @@ -949,6 +949,28 @@ mod tests { } } + #[test] + fn substitute_and_blocked_checks_do_not_pass_involved_acceptance_gate() { + let goal = |relevance| TodoGoal { + difficulty: Some(Difficulty::Involved), + feedback_loop_relevance: Some(relevance), + ..Default::default() + }; + + assert!(!feedback_loop_relevance_passes(&goal( + FeedbackLoopRelevance::Synthetic + ))); + assert!(!feedback_loop_relevance_passes(&goal( + FeedbackLoopRelevance::Representative + ))); + assert!(!feedback_loop_relevance_passes(&goal( + FeedbackLoopRelevance::AcceptanceBlocked + ))); + assert!(feedback_loop_relevance_passes(&goal( + FeedbackLoopRelevance::AcceptanceAligned + ))); + } + /// A score that climbed only after work was underway still gets raised, and /// is described as the coverage gap it is. Suppressing it would let an agent /// clear the gate by writing a good assessment at the end, after the work it @@ -1029,6 +1051,8 @@ mod tests { for guidance in [ "public interfaces", "integration boundaries", + "custom harness", + "externally blocked", "main workflows", "edge cases", "packaging", diff --git a/crates/jcode-task-types/src/lib.rs b/crates/jcode-task-types/src/lib.rs index 9105795155..6fd50353cf 100644 --- a/crates/jcode-task-types/src/lib.rs +++ b/crates/jcode-task-types/src/lib.rs @@ -353,8 +353,10 @@ semantic_state! { /// How directly a goal's feedback loop represents the behavior or outcome /// the user will actually accept. FeedbackLoopRelevance { - Indirect = "indirect", legacy: 0..=49, score: 25, - Representative = "representative", legacy: 50..=95, score: 75, + Indirect = "indirect", legacy: 0..=24, score: 12, + Synthetic = "synthetic", legacy: 25..=49, score: 37, + Representative = "representative", legacy: 50..=79, score: 75, + AcceptanceBlocked = "acceptance_blocked", legacy: 80..=95, score: 88, AcceptanceAligned = "acceptance_aligned", legacy: 96..=100, score: 98, } } @@ -663,6 +665,14 @@ mod semantic_state_tests { serde_json::to_value(FeedbackLoopRelevance::AcceptanceAligned).unwrap(), "acceptance_aligned" ); + assert_eq!( + serde_json::to_value(FeedbackLoopRelevance::Synthetic).unwrap(), + "synthetic" + ); + assert_eq!( + serde_json::to_value(FeedbackLoopRelevance::AcceptanceBlocked).unwrap(), + "acceptance_blocked" + ); assert_eq!( serde_json::to_value(FeedbackLoopCoverage::EdgeAndIntegrationPaths).unwrap(), "edge_and_integration_paths" @@ -726,6 +736,7 @@ mod semantic_state_tests { for score in 0..=100u8 { let _ = IntentUnderstanding::from_legacy_score(score); let _ = FeedbackLoopState::from_legacy_score(score); + let _ = FeedbackLoopRelevance::from_legacy_score(score); let _ = ConfidenceState::from_legacy_score(score); let _ = Difficulty::from_legacy_score(score); let _ = Autonomy::from_legacy_score(score); @@ -733,6 +744,12 @@ mod semantic_state_tests { } assert!(ConfidenceState::Speculative < ConfidenceState::Verified); assert!(DeliveryState::ChangeMade < DeliveryState::WorkflowValidated); + assert!(FeedbackLoopRelevance::Indirect < FeedbackLoopRelevance::Synthetic); + assert!(FeedbackLoopRelevance::Synthetic < FeedbackLoopRelevance::Representative); + assert!(FeedbackLoopRelevance::Representative < FeedbackLoopRelevance::AcceptanceBlocked); + assert!( + FeedbackLoopRelevance::AcceptanceBlocked < FeedbackLoopRelevance::AcceptanceAligned + ); // Representative scores map back onto their own state. for state in [ ConfidenceState::Speculative, From 9abb32d7c69aafc1835294f061ed808d6e6cdcb5 Mon Sep 17 00:00:00 2001 From: jeremy <94247773+1jehuang@users.noreply.github.com> Date: Thu, 6 Aug 2026 01:41:05 -0700 Subject: [PATCH 09/45] fix(memory): distinguish permanent sidecar failures --- crates/jcode-base/src/memory_rerank.rs | 75 +++++++++------ crates/jcode-base/src/sidecar.rs | 124 ++++++++++++++++++++++++- 2 files changed, 168 insertions(+), 31 deletions(-) diff --git a/crates/jcode-base/src/memory_rerank.rs b/crates/jcode-base/src/memory_rerank.rs index 2a415203fd..3ce440d4e6 100644 --- a/crates/jcode-base/src/memory_rerank.rs +++ b/crates/jcode-base/src/memory_rerank.rs @@ -17,11 +17,11 @@ //! memory agent (depends only on `Sidecar` + `MemoryEntry`). use std::collections::HashSet; -use std::sync::Mutex; +use std::sync::{LazyLock, Mutex}; use std::time::{Duration, Instant}; use crate::memory_types::MemoryEntry; -use crate::sidecar::Sidecar; +use crate::sidecar::{Sidecar, SidecarErrorKind}; /// System prompt instructing the model to rank candidates by usefulness. pub const LLM_RERANK_SYSTEM: &str = "You re-rank stored MEMORIES by how useful each would be to surface to an AI coding agent for the CURRENT request. \ @@ -30,8 +30,9 @@ Off-topic, generic, or keyword-only matches rank low. \ Reply with ONLY a JSON array of candidate numbers, best first, e.g. [3,1,7]. Include only clearly useful candidates; omit ones that are not relevant. No prose."; const TRANSIENT_FAILURE_BACKOFF: Duration = Duration::from_secs(30); -const AUTH_FAILURE_BACKOFF: Duration = Duration::from_secs(5 * 60); static FAILURE_BACKOFF_UNTIL: Mutex> = Mutex::new(None); +static REPORTED_PERMANENT_FAILURES: LazyLock>> = + LazyLock::new(|| Mutex::new(HashSet::new())); fn failure_backoff_active() -> bool { let Ok(mut guard) = FAILURE_BACKOFF_UNTIL.lock() else { @@ -47,20 +48,9 @@ fn failure_backoff_active() -> bool { } } -fn record_failure_backoff(error: &anyhow::Error) { - let error = error.to_string().to_ascii_lowercase(); - let permanent_auth_failure = error.contains("401") - || error.contains("unauthorized") - || error.contains("authentication_error") - || error.contains("invalid authentication credentials") - || error.contains("invalid_grant"); - let duration = if permanent_auth_failure { - AUTH_FAILURE_BACKOFF - } else { - TRANSIENT_FAILURE_BACKOFF - }; +fn record_failure_backoff() { if let Ok(mut guard) = FAILURE_BACKOFF_UNTIL.lock() { - let candidate = Instant::now() + duration; + let candidate = Instant::now() + TRANSIENT_FAILURE_BACKOFF; if match *guard { Some(existing) => existing < candidate, None => true, @@ -70,6 +60,20 @@ fn record_failure_backoff(error: &anyhow::Error) { } } +fn report_permanent_failure_once(error: &anyhow::Error) { + let message = error.to_string(); + let should_report = REPORTED_PERMANENT_FAILURES + .lock() + .map(|mut reported| reported.insert(message.clone())) + .unwrap_or(true); + if should_report { + crate::logging::event_error( + "Memory consensus judge permanently misconfigured; fix credentials or the configured memory model", + [("error", message)], + ); + } +} + /// Clear the process-wide judge circuit breaker after real auth state changes. pub(crate) fn clear_failure_backoff() { if let Ok(mut guard) = FAILURE_BACKOFF_UNTIL.lock() { @@ -292,14 +296,19 @@ pub async fn rerank_candidates_consensus_attributed( match sidecar.complete(LLM_RERANK_SYSTEM, &prompt).await { Ok(resp) => extract_ranking(&resp, n), // Some([]) = nothing relevant Err(e) => { - record_failure_backoff(&e); - crate::logging::event_rate_limited( - crate::logging::LogLevel::Warn, - "memory_consensus_judge_failed", - Duration::from_secs(60), - "Memory consensus judge failed; circuit breaker armed", - vec![("error", e.to_string())], - ); + match crate::sidecar::classify_error(&e) { + SidecarErrorKind::Transient => { + record_failure_backoff(); + crate::logging::event_rate_limited( + crate::logging::LogLevel::Warn, + "memory_consensus_judge_failed", + Duration::from_secs(60), + "Memory consensus judge transiently failed; circuit breaker armed", + vec![("error", e.to_string())], + ); + } + SidecarErrorKind::Permanent => report_permanent_failure_once(&e), + } None // transport error = no vote } } @@ -607,14 +616,24 @@ mod tests { } #[test] - fn judge_failure_backoff_arms_and_auth_invalidation_clears_it() { + fn transient_judge_failure_backoff_arms_and_auth_invalidation_clears_it() { clear_failure_backoff(); assert!(!failure_backoff_active()); - record_failure_backoff(&anyhow::anyhow!( - "Claude API error (401 Unauthorized): invalid authentication credentials" - )); + record_failure_backoff(); assert!(failure_backoff_active()); clear_failure_backoff(); assert!(!failure_backoff_active()); } + + #[test] + fn permanent_judge_failure_does_not_arm_backoff() { + clear_failure_backoff(); + let error = anyhow::anyhow!("Claude API error (404 Not Found): not_found_error"); + assert_eq!( + crate::sidecar::classify_error(&error), + SidecarErrorKind::Permanent + ); + report_permanent_failure_once(&error); + assert!(!failure_backoff_active()); + } } diff --git a/crates/jcode-base/src/sidecar.rs b/crates/jcode-base/src/sidecar.rs index 45936bf395..5c01d06211 100644 --- a/crates/jcode-base/src/sidecar.rs +++ b/crates/jcode-base/src/sidecar.rs @@ -11,6 +11,7 @@ use crate::auth; use anyhow::{Context, Result}; use reqwest::StatusCode; use serde::{Deserialize, Serialize}; +use std::fmt; /// Fast/cheap OpenAI model used when Codex credentials are available. pub const SIDECAR_OPENAI_MODEL: &str = "gpt-5.6-luna"; @@ -46,6 +47,80 @@ const CLAUDE_CODE_JCODE_NOTICE: &str = "You are jcode, powered by Claude Code. Y /// Maximum tokens for sidecar responses (keep small for speed/cost) const DEFAULT_MAX_TOKENS: u32 = 1024; +/// Whether retrying a failed sidecar request can reasonably succeed without a +/// configuration or credential change. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SidecarErrorKind { + Transient, + Permanent, +} + +#[derive(Debug)] +struct SidecarHttpError { + provider: &'static str, + status: StatusCode, + body: String, +} + +impl fmt::Display for SidecarHttpError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "{} API error ({}): {}", + self.provider, self.status, self.body + ) + } +} + +impl std::error::Error for SidecarHttpError {} + +/// Classify a sidecar failure for retry policy. HTTP client/auth/request errors +/// are permanent; throttling, server failures, and transport failures are +/// transient. Unknown provider errors retain the conservative retry behavior. +pub fn classify_error(error: &anyhow::Error) -> SidecarErrorKind { + if let Some(error) = error.downcast_ref::() { + return classify_http_status(error.status); + } + for cause in error.chain() { + if let Some(error) = cause.downcast_ref::() { + if let Some(status) = error.status() { + return classify_http_status(status); + } + return SidecarErrorKind::Transient; + } + } + + // Provider-backed sidecars may not expose a typed HTTP error yet. + let message = error.to_string().to_ascii_lowercase(); + if [ + "400", + "401", + "403", + "404", + "bad request", + "unauthorized", + "forbidden", + "not_found_error", + ] + .iter() + .any(|needle| message.contains(needle)) + { + SidecarErrorKind::Permanent + } else { + SidecarErrorKind::Transient + } +} + +fn classify_http_status(status: StatusCode) -> SidecarErrorKind { + if status == StatusCode::TOO_MANY_REQUESTS || status.is_server_error() { + SidecarErrorKind::Transient + } else if status.is_client_error() { + SidecarErrorKind::Permanent + } else { + SidecarErrorKind::Transient + } +} + /// Which backend the sidecar is using #[derive(Debug, Clone, Copy, PartialEq)] enum SidecarBackend { @@ -506,7 +581,12 @@ impl Sidecar { if !response.status().is_success() { let status = response.status(); let error_text = response.text().await.unwrap_or_default(); - anyhow::bail!("Claude API error ({}): {}", status, error_text); + return Err(SidecarHttpError { + provider: "Claude", + status, + body: error_text, + } + .into()); } let result: ClaudeMessagesResponse = response @@ -788,9 +868,12 @@ impl OpenAiSidecarError { fn into_anyhow(self) -> anyhow::Error { match self { - Self::Api { status, body } => { - anyhow::anyhow!("OpenAI API error ({}): {}", status, body) + Self::Api { status, body } => SidecarHttpError { + provider: "OpenAI", + status, + body, } + .into(), Self::Other(err) => err, } } @@ -1040,6 +1123,41 @@ mod tests { assert_eq!(SIDECAR_CLAUDE_MODEL, "claude-haiku-4-5-20251001"); } + #[test] + fn sidecar_http_error_classifies_permanent_client_failures() { + for status in [ + StatusCode::BAD_REQUEST, + StatusCode::UNAUTHORIZED, + StatusCode::FORBIDDEN, + StatusCode::NOT_FOUND, + ] { + let error: anyhow::Error = SidecarHttpError { + provider: "test", + status, + body: "failure".to_string(), + } + .into(); + assert_eq!(classify_error(&error), SidecarErrorKind::Permanent); + } + } + + #[test] + fn sidecar_http_error_classifies_retryable_failures() { + for status in [StatusCode::TOO_MANY_REQUESTS, StatusCode::BAD_GATEWAY] { + let error: anyhow::Error = SidecarHttpError { + provider: "test", + status, + body: "failure".to_string(), + } + .into(); + assert_eq!(classify_error(&error), SidecarErrorKind::Transient); + } + assert_eq!( + classify_error(&anyhow::anyhow!("connection reset")), + SidecarErrorKind::Transient + ); + } + #[test] fn test_backend_selection_prefers_openai() { // Make backend selection deterministic by isolating credentials. From 2a884f275b11b312fd1093b1f4db957ce884985d Mon Sep 17 00:00:00 2001 From: jeremy <94247773+1jehuang@users.noreply.github.com> Date: Thu, 6 Aug 2026 01:41:09 -0700 Subject: [PATCH 10/45] feat(providers): support Meta Muse and DeepSeek passback --- .../src/provider/tests/model_resolution.rs | 15 ++- crates/jcode-base/src/provider_catalog.rs | 6 ++ .../jcode-base/src/provider_catalog_tests.rs | 1 + crates/jcode-provider-core/src/models.rs | 5 + .../src/live_provider_probes.rs | 4 +- crates/jcode-provider-metadata/src/catalog.rs | 30 +++++- .../src/openrouter_provider_impl.rs | 12 ++- .../src/openrouter_tests.rs | 101 ++++++++++++++++++ src/cli/provider_init.rs | 14 +++ src/cli/provider_init_tests.rs | 1 + 10 files changed, 183 insertions(+), 6 deletions(-) diff --git a/crates/jcode-base/src/provider/tests/model_resolution.rs b/crates/jcode-base/src/provider/tests/model_resolution.rs index 0d191cea02..7b449dd80d 100644 --- a/crates/jcode-base/src/provider/tests/model_resolution.rs +++ b/crates/jcode-base/src/provider/tests/model_resolution.rs @@ -2297,11 +2297,12 @@ fn runtime_display_name_tracks_active_openai_compatible_profile() { /// match none of the built-in model-name heuristics and fell through to the /// active provider. #[test] -fn bare_openai_compatible_model_id_routes_to_its_profile_not_the_active_provider() { +fn bare_openai_compatible_model_ids_route_to_their_profile_not_the_active_provider() { with_clean_provider_test_env(|| { let rt = enter_test_runtime(); let _runtime_guard = rt.enter(); crate::env::set_var("CELERIS_API_KEY", "test-celeris-key"); + crate::env::set_var("META_MUSE_API_KEY", "test-meta-key"); let provider = MultiProvider { claude: RwLock::new(None), anthropic: RwLock::new(None), @@ -2324,6 +2325,18 @@ fn bare_openai_compatible_model_id_routes_to_its_profile_not_the_active_provider post_auth_refreshes_pending: Arc::new(std::sync::atomic::AtomicUsize::new(0)), }; + provider + .set_model("muse-spark-1.2") + .expect("bare Muse model id should resolve to the Meta Model API profile"); + assert_eq!(provider.model(), "muse-spark-1.2"); + assert_eq!(provider.active_provider(), ActiveProvider::OpenRouter); + assert_eq!( + provider.fork_model_switch_request(provider.active_provider(), &provider.model()), + "meta-muse:muse-spark-1.2" + ); + + provider.set_active_provider(ActiveProvider::Claude); + provider .set_model("celeris-1") .expect("bare Celeris model id should resolve to the Celeris profile"); diff --git a/crates/jcode-base/src/provider_catalog.rs b/crates/jcode-base/src/provider_catalog.rs index 8c6eaa8379..785874022d 100644 --- a/crates/jcode-base/src/provider_catalog.rs +++ b/crates/jcode-base/src/provider_catalog.rs @@ -496,6 +496,12 @@ pub fn openai_compatible_profile_static_models(profile: OpenAiCompatibleProfile) push("mimo-v2-flash"); push("mimo-v2-omni"); } + // Meta's catalog is authenticated, so expose the documented Muse Spark + // models immediately after login while the live refresh completes. + "meta-muse" => { + push("muse-spark-1.2"); + push("muse-spark-1.1"); + } // MiniMax's `/models` endpoint is authenticated and live, but post-login // model activation should not depend on the catalog refresh completing // before the picker/routes are rebuilt. Keep the documented text models diff --git a/crates/jcode-base/src/provider_catalog_tests.rs b/crates/jcode-base/src/provider_catalog_tests.rs index d8adeada44..2874bd2751 100644 --- a/crates/jcode-base/src/provider_catalog_tests.rs +++ b/crates/jcode-base/src/provider_catalog_tests.rs @@ -1097,6 +1097,7 @@ fn open_weight_family_context_limits_match_published_windows() { assert_eq!(f("kimi-k2.5"), Some(262_144)); assert_eq!(f("minimax-m2.7"), Some(204_800)); assert_eq!(f("mimo-v2.5"), Some(262_144)); + assert_eq!(f("muse-spark-1.2"), Some(1_048_576)); assert_eq!(f("deepseek-v3.2"), Some(163_840)); assert_eq!(f("deepseek-v4-pro"), Some(1_000_000)); assert_eq!(f("qwen3-235b-a22b-instruct-2507"), Some(262_144)); diff --git a/crates/jcode-provider-core/src/models.rs b/crates/jcode-provider-core/src/models.rs index 65d5470826..a8980a1bd6 100644 --- a/crates/jcode-provider-core/src/models.rs +++ b/crates/jcode-provider-core/src/models.rs @@ -355,6 +355,11 @@ pub fn open_weight_family_context_limit(model: &str) -> Option { return Some(262_144); } + // --- Meta Muse Spark family: 1 Mi tokens --- + if m.contains("muse-spark") { + return Some(1_048_576); + } + // --- Alibaba GTE-Qwen2 retrieval models: 32K context --- if m.contains("gte-qwen") { return Some(32_768); diff --git a/crates/jcode-provider-doctor/src/live_provider_probes.rs b/crates/jcode-provider-doctor/src/live_provider_probes.rs index ccd53dae58..d455a1738e 100644 --- a/crates/jcode-provider-doctor/src/live_provider_probes.rs +++ b/crates/jcode-provider-doctor/src/live_provider_probes.rs @@ -507,7 +507,9 @@ pub async fn run_live_openai_compatible_tool_smoke( ], "stream": false }); - set_output_token_cap(&mut body, &resolved, 256); + // Reasoning models can consume hundreds of hidden tokens before emitting a + // tool call. Leave enough room to test the tool path rather than truncation. + set_output_token_cap(&mut body, &resolved, 1_024); if !resolved.api_base.contains("fptcloud.com") { body["tool_choice"] = serde_json::json!("auto"); } diff --git a/crates/jcode-provider-metadata/src/catalog.rs b/crates/jcode-provider-metadata/src/catalog.rs index 1ae609f9c5..d30bec712f 100644 --- a/crates/jcode-provider-metadata/src/catalog.rs +++ b/crates/jcode-provider-metadata/src/catalog.rs @@ -408,6 +408,17 @@ pub const XIAOMI_MIMO_PROFILE: OpenAiCompatibleProfile = OpenAiCompatibleProfile requires_api_key: true, }; +pub const META_MUSE_PROFILE: OpenAiCompatibleProfile = OpenAiCompatibleProfile { + id: "meta-muse", + display_name: "Meta Model API", + api_base: "https://api.meta.ai/v1", + api_key_env: "META_MUSE_API_KEY", + env_file: "meta-muse.env", + setup_url: "https://dev.meta.ai/", + default_model: Some("muse-spark-1.2"), + requires_api_key: true, +}; + pub const CELERIS_PROFILE: OpenAiCompatibleProfile = OpenAiCompatibleProfile { id: "celeris", display_name: "Celeris", @@ -432,7 +443,7 @@ pub const OPENAI_COMPAT_PROFILE: OpenAiCompatibleProfile = OpenAiCompatibleProfi requires_api_key: true, }; -pub(crate) const OPENAI_COMPAT_PROFILES: [OpenAiCompatibleProfile; 37] = [ +pub(crate) const OPENAI_COMPAT_PROFILES: [OpenAiCompatibleProfile; 38] = [ OPENCODE_PROFILE, OPENCODE_GO_PROFILE, ZAI_PROFILE, @@ -466,6 +477,7 @@ pub(crate) const OPENAI_COMPAT_PROFILES: [OpenAiCompatibleProfile; 37] = [ XAI_PROFILE, NVIDIA_NIM_PROFILE, XIAOMI_MIMO_PROFILE, + META_MUSE_PROFILE, CELERIS_PROFILE, LMSTUDIO_PROFILE, OLLAMA_PROFILE, @@ -1086,6 +1098,19 @@ pub const XIAOMI_MIMO_LOGIN_PROVIDER: LoginProviderDescriptor = LoginProviderDes order: LoginProviderSurfaceOrder::new(Some(37), Some(37), Some(37), Some(37), Some(37)), }; +pub const META_MUSE_LOGIN_PROVIDER: LoginProviderDescriptor = LoginProviderDescriptor { + id: "meta-muse", + display_name: "Meta Model API", + auth_kind: LoginProviderAuthKind::ApiKey, + auth_state_key: LoginProviderAuthStateKey::OpenRouterLike, + auth_status_method: "API key", + aliases: &["meta", "muse", "muse-spark", "meta-model-api", "meta-ai"], + menu_detail: "OpenAI-compatible Meta Model API", + recommended: false, + target: LoginProviderTarget::OpenAiCompatible(META_MUSE_PROFILE), + order: LoginProviderSurfaceOrder::new(Some(38), Some(38), Some(38), Some(38), Some(38)), +}; + pub const CELERIS_LOGIN_PROVIDER: LoginProviderDescriptor = LoginProviderDescriptor { id: "celeris", display_name: "Celeris", @@ -1112,7 +1137,7 @@ pub const GOOGLE_LOGIN_PROVIDER: LoginProviderDescriptor = LoginProviderDescript order: LoginProviderSurfaceOrder::new(Some(13), None, None, None, None), }; -pub(crate) const LOGIN_PROVIDERS: [LoginProviderDescriptor; 48] = [ +pub(crate) const LOGIN_PROVIDERS: [LoginProviderDescriptor; 49] = [ AUTO_IMPORT_LOGIN_PROVIDER, CLAUDE_LOGIN_PROVIDER, ANTHROPIC_API_LOGIN_PROVIDER, @@ -1151,6 +1176,7 @@ pub(crate) const LOGIN_PROVIDERS: [LoginProviderDescriptor; 48] = [ XAI_LOGIN_PROVIDER, NVIDIA_NIM_LOGIN_PROVIDER, XIAOMI_MIMO_LOGIN_PROVIDER, + META_MUSE_LOGIN_PROVIDER, CELERIS_LOGIN_PROVIDER, LMSTUDIO_LOGIN_PROVIDER, OLLAMA_LOGIN_PROVIDER, diff --git a/crates/jcode-provider-openrouter-runtime/src/openrouter_provider_impl.rs b/crates/jcode-provider-openrouter-runtime/src/openrouter_provider_impl.rs index a812fc6b92..86b2bd8234 100644 --- a/crates/jcode-provider-openrouter-runtime/src/openrouter_provider_impl.rs +++ b/crates/jcode-provider-openrouter-runtime/src/openrouter_provider_impl.rs @@ -60,8 +60,16 @@ impl Provider for OpenRouterProvider { None } }); - let allow_reasoning = (self.supports_provider_features || kimi_coding_endpoint) - && thinking_enabled != Some(false); + // DeepSeek-family models served through a direct OpenAI-compatible + // profile can run thinking mode server-side. Their follow-up requests + // must replay the `reasoning_content` returned with an assistant tool + // call even though the route has no OpenRouter provider features + // (issue #815). Unlike Kimi, this only unlocks stored reasoning: it does + // not synthesize the field when the prior turn did not return one. + let deepseek_model = Self::model_is_deepseek_family(&model); + let allow_reasoning = + (self.supports_provider_features || kimi_coding_endpoint || deepseek_model) + && thinking_enabled != Some(false); let include_reasoning_content = thinking_enabled == Some(true) || (allow_reasoning && Self::is_kimi_model(&model)) || kimi_coding_endpoint; diff --git a/crates/jcode-provider-openrouter-runtime/src/openrouter_tests.rs b/crates/jcode-provider-openrouter-runtime/src/openrouter_tests.rs index cb1367b1fe..cfe2fa70f0 100644 --- a/crates/jcode-provider-openrouter-runtime/src/openrouter_tests.rs +++ b/crates/jcode-provider-openrouter-runtime/src/openrouter_tests.rs @@ -713,6 +713,106 @@ fn kimi_for_coding_tool_call_message_includes_reasoning_content() { ); } +/// Regression for issue #815: DeepSeek-family models on direct +/// OpenAI-compatible profiles require the reasoning returned alongside an +/// assistant tool call to be replayed on the next request. These routes do not +/// enable OpenRouter provider features, so model-family detection must unlock +/// the stored `reasoning_content` without adding a top-level thinking config. +#[test] +fn direct_compatible_deepseek_tool_call_replays_reasoning_content() { + let _lock = ENV_LOCK.lock(); + let _thinking = EnvVarGuard::remove("JCODE_OPENROUTER_THINKING"); + let (api_base, request_rx) = spawn_single_response_chat_server(); + let provider = OpenRouterProvider { + api_base, + profile_id: Some("opencode-zen".to_string()), + supports_provider_features: false, + supports_model_catalog: false, + model: Arc::new(RwLock::new("deepseek-v4-flash-free".to_string())), + ..make_custom_compatible_provider() + }; + + let messages = vec![ + Message { + role: Role::User, + content: vec![ContentBlock::Text { + text: "list the files".to_string(), + cache_control: None, + }], + timestamp: None, + tool_duration_ms: None, + }, + Message { + role: Role::Assistant, + content: vec![ + ContentBlock::Reasoning { + text: "I should inspect the workspace first.".to_string(), + }, + ContentBlock::ToolUse { + id: "call_1".to_string(), + name: "bash".to_string(), + input: serde_json::json!({"command": "ls"}), + thought_signature: None, + }, + ], + timestamp: None, + tool_duration_ms: None, + }, + Message { + role: Role::User, + content: vec![ContentBlock::ToolResult { + tool_use_id: "call_1".to_string(), + content: "a.txt\nb.txt".to_string(), + is_error: None, + }], + timestamp: None, + tool_duration_ms: None, + }, + ]; + + let rt = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .expect("runtime"); + rt.block_on(async { + let mut stream = provider + .complete(&messages, &[], "", None) + .await + .expect("fake chat request should start"); + while let Some(event) = stream.next().await { + if event.is_err() { + break; + } + } + }); + + let request = request_rx + .recv_timeout(Duration::from_secs(2)) + .expect("capture fake provider request"); + let body = parse_captured_request_body(&request); + let assistant = body["messages"] + .as_array() + .expect("request should contain messages array") + .iter() + .find(|message| { + message.get("role").and_then(|value| value.as_str()) == Some("assistant") + && message.get("tool_calls").is_some() + }) + .expect("request should retain the assistant tool-call turn"); + + assert_eq!( + assistant + .get("reasoning_content") + .and_then(|value| value.as_str()), + Some("I should inspect the workspace first."), + "direct DeepSeek request must replay stored reasoning_content (issue #815): {assistant}" + ); + assert!( + body.get("thinking").is_none(), + "server-managed thinking must not add OpenRouter's top-level thinking field: {body}" + ); +} + #[test] fn minimax_profile_exposes_static_models_before_catalog_refresh() { let models = jcode_base::provider_catalog::openai_compatible_profile_static_models( @@ -789,6 +889,7 @@ fn openai_compatible_profiles_with_unverified_live_catalogs_have_static_fallback "accounts/fireworks/routers/kimi-k2p5-turbo", ), (jcode_provider_metadata::XIAOMI_MIMO_PROFILE, "mimo-v2.5"), + (jcode_provider_metadata::META_MUSE_PROFILE, "muse-spark-1.2"), ( jcode_provider_metadata::ALIBABA_CODING_PLAN_PROFILE, "qwen3-coder-plus", diff --git a/src/cli/provider_init.rs b/src/cli/provider_init.rs index 85b8055a9a..0ceb32570b 100644 --- a/src/cli/provider_init.rs +++ b/src/cli/provider_init.rs @@ -94,6 +94,14 @@ pub enum ProviderChoice { NvidiaNim, #[value(alias = "xiaomi", alias = "mimo", alias = "xiaomi-mimo-api")] XiaomiMimo, + #[value( + alias = "meta", + alias = "muse", + alias = "muse-spark", + alias = "meta-model-api", + alias = "meta-ai" + )] + MetaMuse, #[value(alias = "celeris-ai", alias = "celeris1", alias = "celeris-1")] Celeris, #[value(alias = "lm-studio")] @@ -165,6 +173,7 @@ impl ProviderChoice { Self::Xai => "xai", Self::NvidiaNim => "nvidia-nim", Self::XiaomiMimo => "xiaomi-mimo", + Self::MetaMuse => "meta-muse", Self::Celeris => "celeris", Self::Lmstudio => "lmstudio", Self::Ollama => "ollama", @@ -325,6 +334,10 @@ const PROVIDER_CHOICE_LOGIN_PROVIDERS: &[(ProviderChoice, LoginProviderDescripto ProviderChoice::XiaomiMimo, crate::provider_catalog::XIAOMI_MIMO_LOGIN_PROVIDER, ), + ( + ProviderChoice::MetaMuse, + crate::provider_catalog::META_MUSE_LOGIN_PROVIDER, + ), ( ProviderChoice::Celeris, crate::provider_catalog::CELERIS_LOGIN_PROVIDER, @@ -1529,6 +1542,7 @@ async fn init_provider_with_options( | ProviderChoice::Xai | ProviderChoice::NvidiaNim | ProviderChoice::XiaomiMimo + | ProviderChoice::MetaMuse | ProviderChoice::Celeris | ProviderChoice::Lmstudio | ProviderChoice::Ollama diff --git a/src/cli/provider_init_tests.rs b/src/cli/provider_init_tests.rs index 067bcc7340..b5a90e465c 100644 --- a/src/cli/provider_init_tests.rs +++ b/src/cli/provider_init_tests.rs @@ -47,6 +47,7 @@ fn test_provider_choice_arg_values() { assert_eq!(ProviderChoice::Minimax.as_arg_value(), "minimax"); assert_eq!(ProviderChoice::Xai.as_arg_value(), "xai"); assert_eq!(ProviderChoice::XiaomiMimo.as_arg_value(), "xiaomi-mimo"); + assert_eq!(ProviderChoice::MetaMuse.as_arg_value(), "meta-muse"); assert_eq!(ProviderChoice::Celeris.as_arg_value(), "celeris"); assert_eq!(ProviderChoice::Lmstudio.as_arg_value(), "lmstudio"); assert_eq!(ProviderChoice::Ollama.as_arg_value(), "ollama"); From c31dfd4e7edfeb5a29552bd1fd4b60dd194150d9 Mon Sep 17 00:00:00 2001 From: jeremy <94247773+1jehuang@users.noreply.github.com> Date: Thu, 6 Aug 2026 01:51:12 -0700 Subject: [PATCH 11/45] docs(providers): add Meta Model API setup --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index c3dc748831..01b11b79d3 100644 --- a/README.md +++ b/README.md @@ -338,6 +338,7 @@ jcode works with subscription-backed OAuth flows and many provider integrations, - **Alibaba Cloud Coding Plan** (`jcode login --provider alibaba-coding-plan`) - **Fireworks** (`jcode login --provider fireworks`) - **MiniMax** (`jcode login --provider minimax`) +- **Meta Model API / Muse** (`jcode login --provider meta-muse`) - **LM Studio** (`jcode login --provider lmstudio`) - **Ollama** (`jcode login --provider ollama`) - **Custom OpenAI-compatible endpoint** (`jcode login --provider openai-compatible`) @@ -363,9 +364,10 @@ There are two ways to set one up: jcode login --provider deepseek jcode login --provider opencode # OpenCode Zen jcode login --provider moonshotai + jcode login --provider meta-muse # Meta Model API / Muse Spark ``` - Built-in OpenAI-compatible profile ids include: `openrouter`, `deepseek`, `zai`, `kimi`, `moonshotai`, `opencode` (OpenCode Zen), `opencode-go`, `302ai`, `baseten`, `cortecs`, `huggingface`, `nebius`, `scaleway`, `stackit`, and `firmware`. Each profile only sets the endpoint and key variable; you still pick the model with `/model` (or `--model`). Run `jcode login` with no provider to see the interactive list. + Built-in OpenAI-compatible profile ids include: `openrouter`, `deepseek`, `zai`, `kimi`, `moonshotai`, `meta-muse` (Meta Model API / Muse Spark), `opencode` (OpenCode Zen), `opencode-go`, `302ai`, `baseten`, `cortecs`, `huggingface`, `nebius`, `scaleway`, `stackit`, and `firmware`. Each profile only sets the endpoint and key variable; you still pick the model with `/model` (or `--model`). Run `jcode login` with no provider to see the interactive list. - **Any other endpoint** — point jcode at an arbitrary OpenAI-compatible API (hosted or local) with `jcode login --provider openai-compatible` or the scriptable `jcode provider add` command described below. From 25463c35c993693e06cd40c78733171f4b0f7917 Mon Sep 17 00:00:00 2001 From: jeremy <94247773+1jehuang@users.noreply.github.com> Date: Thu, 6 Aug 2026 02:03:03 -0700 Subject: [PATCH 12/45] feat(todo): require requirement traceability --- crates/jcode-app-core/src/tool/todo.rs | 49 +++++- crates/jcode-base/src/session/persistence.rs | 157 +++++++++++++++++- crates/jcode-base/src/todo.rs | 70 +++++++- crates/jcode-task-types/src/lib.rs | 19 +++ crates/jcode-telemetry-core/src/lib.rs | 8 +- crates/jcode-tui/src/tui/app/todos_view.rs | 7 + crates/jcode-tui/src/tui/info_widget_todos.rs | 3 + crates/jcode-tui/src/tui/ui_messages.rs | 42 ++++- 8 files changed, 345 insertions(+), 10 deletions(-) diff --git a/crates/jcode-app-core/src/tool/todo.rs b/crates/jcode-app-core/src/tool/todo.rs index 7f3e0aad56..b2d9ebea8d 100644 --- a/crates/jcode-app-core/src/tool/todo.rs +++ b/crates/jcode-app-core/src/tool/todo.rs @@ -227,6 +227,9 @@ fn merge_goals(stored: &[TodoGoal], incoming: Option>) -> Vec>) -> Vec>) -> Vec, after: Option<&TodoGoal>) -> V { fields.push(TodoGoalField::FeedbackLoopCoverage); } + if before.and_then(|goal| goal.feedback_loop_traceability) + != after.and_then(|goal| goal.feedback_loop_traceability) + { + fields.push(TodoGoalField::FeedbackLoopTraceability); + } if before.and_then(|goal| goal.delivery_state) != after.and_then(|goal| goal.delivery_state) { fields.push(TodoGoalField::DeliveryState); } @@ -520,6 +535,15 @@ fn record_reframe_observations( .map(|state| state.as_str().to_string()), }); } + if !crate::todo::feedback_loop_traceability_passes(goal) { + observations.push(GateObservation { + kind: GateObservationKind::FeedbackLoopTraceability, + group: goal.group.clone(), + state: goal + .feedback_loop_traceability + .map(|state| state.as_str().to_string()), + }); + } } (observations, immediate) } @@ -645,6 +669,7 @@ fn normalize_todo_input(mut input: Value) -> Value { "delivery_state", "feedback_loop_relevance", "feedback_loop_coverage", + "feedback_loop_traceability", "difficulty", "autonomy", ] { @@ -750,7 +775,7 @@ impl Tool for TodoTool { "description": "Goal-level assessments, one per todo group (null = ungrouped). Omitted groups are retained.", "items": { "type": "object", - "required": ["closed_feedback_loop", "feedback_loop", "feedback_loop_relevance", "feedback_loop_coverage"], + "required": ["closed_feedback_loop", "feedback_loop", "feedback_loop_relevance", "feedback_loop_coverage", "feedback_loop_traceability"], "properties": { "group": { "type": "string", @@ -775,6 +800,11 @@ impl Tool for TodoTool { "enum": ["narrow", "main_paths", "edge_and_integration_paths"], "description": "How broadly the checks exercise main workflows, integration boundaries, edge cases, packaging, and likely failure modes." }, + "feedback_loop_traceability": { + "type": "string", + "enum": ["unmapped", "partial", "complete"], + "description": "How completely requirements map to evidence. unmapped = requirements are not tied to checks; partial = only some explicit requirements or changed public outputs have concrete checks and observed results; complete = every explicit requirement and changed public output has a concrete check and observed result. Aggregate test counts alone do not establish complete traceability." + }, "delivery_state": { "type": "string", "enum": ["change_made", "integrated", "workflow_validated", "outcome_delivered"], @@ -836,6 +866,9 @@ impl Tool for TodoTool { GateObservationKind::FeedbackLoopCoverage => { crate::telemetry::TodoGateKind::FeedbackLoopCoverage } + GateObservationKind::FeedbackLoopTraceability => { + crate::telemetry::TodoGateKind::FeedbackLoopTraceability + } }; crate::telemetry::record_todo_gate(kind); } @@ -972,6 +1005,7 @@ mod tests { assert!(goal_props.contains_key("feedback_loop")); assert!(goal_props.contains_key("feedback_loop_relevance")); assert!(goal_props.contains_key("feedback_loop_coverage")); + assert!(goal_props.contains_key("feedback_loop_traceability")); assert!(goal_props.contains_key("delivery_state")); assert!(goal_props.contains_key("difficulty")); assert!(goal_props.contains_key("autonomy")); @@ -982,7 +1016,7 @@ mod tests { assert!(!goal_props.contains_key("user_intention")); assert!(!goal_props.contains_key("alignment_score")); assert!(!goal_props.contains_key("objective")); - assert_eq!(goal_props.len(), 10); + assert_eq!(goal_props.len(), 11); assert_eq!( goal_props["feedback_loop_relevance"]["enum"], json!([ @@ -1024,6 +1058,11 @@ mod tests { .iter() .any(|value| value == "feedback_loop_coverage") ); + assert!( + goal_required + .iter() + .any(|value| value == "feedback_loop_traceability") + ); let alignment_description = plan_props["understands_user_intent"] .get("description") @@ -1298,6 +1337,7 @@ mod tests { closed_feedback_loop: Some(state), feedback_loop_relevance: Some(crate::todo::FeedbackLoopRelevance::Representative), feedback_loop_coverage: Some(crate::todo::FeedbackLoopCoverage::MainPaths), + feedback_loop_traceability: Some(crate::todo::FeedbackLoopTraceability::Complete), ..Default::default() } } @@ -1771,7 +1811,7 @@ mod tests { // The points were recorded for the turn-end digest instead. let observations = crate::todo::load_gate_observations(session).expect("observations"); - assert_eq!(observations.len(), 4); + assert_eq!(observations.len(), 5); assert!( observations.iter().any(|observation| { observation.kind == GateObservationKind::FeedbackLoopRelevance @@ -1782,6 +1822,9 @@ mod tests { observation.kind == GateObservationKind::FeedbackLoopCoverage }) ); + assert!(observations.iter().any(|observation| { + observation.kind == GateObservationKind::FeedbackLoopTraceability + })); // Histories are accumulating, which is what the digest reasons over. let plan = load_plan(session).expect("plan"); diff --git a/crates/jcode-base/src/session/persistence.rs b/crates/jcode-base/src/session/persistence.rs index f7f6af5a69..8eab530131 100644 --- a/crates/jcode-base/src/session/persistence.rs +++ b/crates/jcode-base/src/session/persistence.rs @@ -1,7 +1,7 @@ -use anyhow::Result; +use anyhow::{Result, bail}; use chrono::Utc; use std::io::{BufRead, BufReader}; -use std::path::Path; +use std::path::{Path, PathBuf}; use std::time::Instant; use super::journal::{PersistVectorMode, SessionJournalEntry, metadata_requires_snapshot}; @@ -129,6 +129,52 @@ fn replay_journal_lines( } impl Session { + fn pre_wipe_backup_path(path: &Path, timestamp: i64) -> PathBuf { + let file_name = path + .file_name() + .map(|name| name.to_string_lossy()) + .unwrap_or_default(); + path.with_file_name(format!("{file_name}.pre-wipe-{timestamp}.bak")) + } + + /// Preserve the last durable transcript before an empty in-memory session + /// can replace it. This is deliberately best-effort: the hard rejection in + /// `checkpoint_snapshot` is what prevents data loss, while these copies make + /// recovery possible if another caller ever bypasses that invariant. + fn guard_snapshot_shrink(&self, snapshot_path: &Path, journal_path: &Path) { + const MIN_TRANSCRIPT_SNAPSHOT_BYTES: u64 = 4 * 1024; + + if !self.messages.is_empty() + || file_len_or_zero(snapshot_path) <= MIN_TRANSCRIPT_SNAPSHOT_BYTES + { + return; + } + + let timestamp = Utc::now().timestamp_millis(); + let snapshot_backup = Self::pre_wipe_backup_path(snapshot_path, timestamp); + let journal_backup = Self::pre_wipe_backup_path(journal_path, timestamp); + let mut backup_errors = Vec::new(); + + if let Err(err) = std::fs::copy(snapshot_path, &snapshot_backup) { + backup_errors.push(format!("snapshot {}: {err}", snapshot_backup.display())); + } + if journal_path.exists() + && let Err(err) = std::fs::copy(journal_path, &journal_backup) + { + backup_errors.push(format!("journal {}: {err}", journal_backup.display())); + } + + let fields = vec![ + ("phase", "pre_wipe_backup".to_string()), + ("session_id", self.id.clone()), + ("snapshot_path", snapshot_path.display().to_string()), + ("snapshot_backup", snapshot_backup.display().to_string()), + ("journal_backup", journal_backup.display().to_string()), + ("errors", backup_errors.join("; ")), + ]; + crate::logging::event_warn("SESSION_PERSISTENCE", fields); + } + fn apply_journal_entry(&mut self, entry: SessionJournalEntry) { self.apply_journal_meta(entry.meta); self.messages.extend(entry.append_messages); @@ -140,6 +186,17 @@ impl Session { } fn checkpoint_snapshot(&mut self, snapshot_path: &Path, journal_path: &Path) -> Result<()> { + let destructive_empty_checkpoint = self.messages.is_empty() + && self.persist_state.messages_len > 0 + && snapshot_path.exists(); + if destructive_empty_checkpoint { + self.guard_snapshot_shrink(snapshot_path, journal_path); + bail!( + "refusing to replace non-empty persisted transcript for session {} with an empty checkpoint", + self.id + ); + } + self.guard_snapshot_shrink(snapshot_path, journal_path); storage::write_json_fast(snapshot_path, self)?; if journal_path.exists() { let _ = std::fs::remove_file(journal_path); @@ -505,3 +562,99 @@ impl Session { result } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::message::{ContentBlock, Role}; + + fn pre_wipe_backups(dir: &Path) -> Vec { + std::fs::read_dir(dir) + .unwrap() + .filter_map(|entry| entry.ok().map(|entry| entry.path())) + .filter(|path| path.to_string_lossy().contains(".pre-wipe-")) + .collect() + } + + #[test] + fn empty_checkpoint_preserves_and_refuses_to_wipe_large_transcript() { + let dir = tempfile::tempdir().unwrap(); + let snapshot_path = dir.path().join("session_guard.json"); + let journal_path = dir.path().join("session_guard.jsonl"); + let original_snapshot = vec![b'x'; 5 * 1024]; + let original_journal = b"durable journal tail\n"; + std::fs::write(&snapshot_path, &original_snapshot).unwrap(); + std::fs::write(&journal_path, original_journal).unwrap(); + + let mut session = Session::create_with_id("session_guard".into(), None, None); + session.persist_state.snapshot_exists = true; + session.persist_state.messages_len = 686; + + let error = session + .checkpoint_snapshot(&snapshot_path, &journal_path) + .unwrap_err(); + assert!(error.to_string().contains("refusing to replace")); + assert_eq!(std::fs::read(&snapshot_path).unwrap(), original_snapshot); + assert_eq!(std::fs::read(&journal_path).unwrap(), original_journal); + + let backups = pre_wipe_backups(dir.path()); + assert_eq!(backups.len(), 2); + assert!(backups.iter().any(|path| { + path.to_string_lossy().contains(".json.pre-wipe-") + && std::fs::read(path).unwrap() == original_snapshot + })); + assert!(backups.iter().any(|path| { + path.to_string_lossy().contains(".jsonl.pre-wipe-") + && std::fs::read(path).unwrap() == original_journal + })); + } + + #[test] + fn non_empty_shrink_checkpoint_remains_allowed_without_pre_wipe_backup() { + let dir = tempfile::tempdir().unwrap(); + let snapshot_path = dir.path().join("session_compacted.json"); + let journal_path = dir.path().join("session_compacted.jsonl"); + std::fs::write(&snapshot_path, vec![b'x'; 5 * 1024]).unwrap(); + std::fs::write(&journal_path, b"old journal\n").unwrap(); + + let mut session = Session::create_with_id("session_compacted".into(), None, None); + session.add_message( + Role::User, + vec![ContentBlock::Text { + text: "retained compacted message".into(), + cache_control: None, + }], + ); + session.persist_state.snapshot_exists = true; + session.persist_state.messages_len = 2; + + session + .checkpoint_snapshot(&snapshot_path, &journal_path) + .unwrap(); + assert!(!journal_path.exists()); + assert!(pre_wipe_backups(dir.path()).is_empty()); + let restored: Session = storage::read_json(&snapshot_path).unwrap(); + assert_eq!(restored.messages.len(), 1); + } + + #[test] + fn small_empty_snapshot_is_rejected_without_creating_noise_backup() { + let dir = tempfile::tempdir().unwrap(); + let snapshot_path = dir.path().join("session_small.json"); + let journal_path = dir.path().join("session_small.jsonl"); + let original = b"small metadata stub"; + std::fs::write(&snapshot_path, original).unwrap(); + + let mut session = Session::create_with_id("session_small".into(), None, None); + session.persist_state.snapshot_exists = true; + session.persist_state.messages_len = 1; + + assert!( + session + .checkpoint_snapshot(&snapshot_path, &journal_path) + .is_err() + ); + assert_eq!(std::fs::read(&snapshot_path).unwrap(), original); + assert!(pre_wipe_backups(dir.path()).is_empty()); + } +} diff --git a/crates/jcode-base/src/todo.rs b/crates/jcode-base/src/todo.rs index d64fadd13e..8dac3ead30 100644 --- a/crates/jcode-base/src/todo.rs +++ b/crates/jcode-base/src/todo.rs @@ -21,8 +21,9 @@ struct TodoReviewState { pub use jcode_task_types::{ Autonomy, ConfidenceState, DeliveryState, Difficulty, FeedbackLoopCoverage, - FeedbackLoopRelevance, FeedbackLoopState, IntentUnderstanding, IterationMaturity, TodoGoal, - TodoGoalChange, TodoGoalField, TodoItem, TodoPlan, TodoPlanChange, TodoPlanField, + FeedbackLoopRelevance, FeedbackLoopState, FeedbackLoopTraceability, IntentUnderstanding, + IterationMaturity, TodoGoal, TodoGoalChange, TodoGoalField, TodoItem, TodoPlan, TodoPlanChange, + TodoPlanField, }; /// Whether the plan's intent understanding is solid enough to work against. @@ -66,6 +67,21 @@ pub fn feedback_loop_coverage_passes(goal: &TodoGoal) -> bool { .is_some_and(|state| state >= required_feedback_loop_coverage(goal.difficulty)) } +pub fn required_feedback_loop_traceability( + difficulty: Option, +) -> FeedbackLoopTraceability { + if difficulty.is_some_and(|difficulty| difficulty >= Difficulty::Involved) { + FeedbackLoopTraceability::Complete + } else { + FeedbackLoopTraceability::Partial + } +} + +pub fn feedback_loop_traceability_passes(goal: &TodoGoal) -> bool { + goal.feedback_loop_traceability + .is_some_and(|state| state >= required_feedback_loop_traceability(goal.difficulty)) +} + /// Whether a completed todo carries enough evidence behind its completion. pub fn completion_confidence_passes(state: Option) -> bool { state.is_some_and(|state| state >= ConfidenceState::Validated) @@ -107,6 +123,7 @@ pub fn delivery_state_passes(goal: &TodoGoal) -> bool { && stopping_evidence_passes && feedback_loop_relevance_passes(goal) && feedback_loop_coverage_passes(goal) + && feedback_loop_traceability_passes(goal) } /// Pre-plan-intent-rewrite alignment continuation. Kept only so persisted @@ -193,6 +210,12 @@ pub fn build_todo_ownership_continuation_message(todos: &[TodoItem], goals: &[To label )); } + if !feedback_loop_traceability_passes(goal) { + message.push_str(&format!( + "\n- Goal \"{}\": map every explicit requirement and changed public output to a concrete check and report its observed result.", + label + )); + } if matches!( goal.iteration_maturity, Some( @@ -244,6 +267,7 @@ pub enum GateObservationKind { ClosedFeedbackLoop, FeedbackLoopRelevance, FeedbackLoopCoverage, + FeedbackLoopTraceability, } /// A point during the turn that would previously have interrupted the model @@ -304,6 +328,10 @@ fn observation_score_later_cleared( .iter() .find(|goal| normalized_group(goal.group.as_deref()) == observation.group) .is_some_and(feedback_loop_coverage_passes), + GateObservationKind::FeedbackLoopTraceability => goals + .iter() + .find(|goal| normalized_group(goal.group.as_deref()) == observation.group) + .is_some_and(feedback_loop_traceability_passes), } } @@ -410,6 +438,26 @@ pub fn build_gate_digest( label ) } + (GateObservationKind::FeedbackLoopTraceability, false) => { + let label = group + .as_deref() + .map(|group| format!(" for \"{}\"", group)) + .unwrap_or_default(); + format!( + "the checks{} were not traced to every explicit requirement and changed public output. Map each one to a concrete check and report the observed result; aggregate test counts do not establish this mapping.", + label + ) + } + (GateObservationKind::FeedbackLoopTraceability, true) => { + let label = group + .as_deref() + .map(|group| format!(" for \"{}\"", group)) + .unwrap_or_default(); + format!( + "complete requirement-to-check traceability{} was identified only after earlier work was done. Run every mapped check over the whole result now and report what each requirement and changed public output actually did.", + label + ) + } }; let repeats = if *count > 1 { format!(" (flagged {} times this turn)", count) @@ -1546,6 +1594,7 @@ mod tests { iteration_maturity: Some(IterationMaturity::OutcomeReached), feedback_loop_relevance: Some(FeedbackLoopRelevance::Representative), feedback_loop_coverage: Some(FeedbackLoopCoverage::MainPaths), + feedback_loop_traceability: Some(FeedbackLoopTraceability::Complete), ..Default::default() } } @@ -1645,6 +1694,23 @@ mod tests { assert!(!delivery_state_passes(&involved)); } + #[test] + fn feedback_loop_traceability_scales_at_involved_difficulty() { + let mut goal = delivery_goal(None, Some(DeliveryState::WorkflowValidated)); + goal.feedback_loop_traceability = Some(FeedbackLoopTraceability::Partial); + assert!(delivery_state_passes(&goal)); + + goal.difficulty = Some(Difficulty::Involved); + goal.feedback_loop_relevance = Some(FeedbackLoopRelevance::AcceptanceAligned); + goal.feedback_loop_coverage = Some(FeedbackLoopCoverage::EdgeAndIntegrationPaths); + assert!(!delivery_state_passes(&goal)); + + goal.feedback_loop_traceability = Some(FeedbackLoopTraceability::Complete); + assert!(delivery_state_passes(&goal)); + goal.feedback_loop_traceability = None; + assert!(!delivery_state_passes(&goal)); + } + #[test] fn research_completion_requires_stopping_evidence() { let previous = vec![todo("work", "in_progress", Some("ship"))]; diff --git a/crates/jcode-task-types/src/lib.rs b/crates/jcode-task-types/src/lib.rs index 6fd50353cf..576f2c64c1 100644 --- a/crates/jcode-task-types/src/lib.rs +++ b/crates/jcode-task-types/src/lib.rs @@ -371,6 +371,16 @@ semantic_state! { } } +semantic_state! { + /// How completely a goal's explicit requirements and changed public outputs + /// are connected to concrete checks and observed results. + FeedbackLoopTraceability { + Unmapped = "unmapped", legacy: 0..=49, score: 25, + Partial = "partial", legacy: 50..=95, score: 75, + Complete = "complete", legacy: 96..=100, score: 98, + } +} + semantic_state! { /// Evidence state behind a todo: from an unexamined guess to a result /// verified end to end. @@ -554,6 +564,14 @@ pub struct TodoGoal { /// oldest first. Tool-maintained; model-supplied values are ignored. #[serde(default, skip_serializing_if = "Vec::is_empty")] pub feedback_loop_coverage_history: Vec, + /// Whether every explicit requirement and changed public output is mapped to + /// a concrete check and its observed result. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub feedback_loop_traceability: Option, + /// Every distinct `feedback_loop_traceability` state this goal has carried, + /// oldest first. Tool-maintained; model-supplied values are ignored. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub feedback_loop_traceability_history: Vec, /// How far the goal's result actually traveled toward the user's outcome: /// from a bare change through integration and workflow validation to a /// delivered outcome. Replaces the legacy 0-100 `end_to_end_ownership` @@ -604,6 +622,7 @@ pub enum TodoGoalField { FeedbackLoop, FeedbackLoopRelevance, FeedbackLoopCoverage, + FeedbackLoopTraceability, #[serde(alias = "end_to_end_ownership")] DeliveryState, Autonomy, diff --git a/crates/jcode-telemetry-core/src/lib.rs b/crates/jcode-telemetry-core/src/lib.rs index d1d7eed9bf..ec484ef9c4 100644 --- a/crates/jcode-telemetry-core/src/lib.rs +++ b/crates/jcode-telemetry-core/src/lib.rs @@ -2174,6 +2174,8 @@ pub enum TodoGateKind { FeedbackLoopRelevance, /// Completion checks did not cover enough success and failure paths. FeedbackLoopCoverage, + /// Requirements or changed outputs were not mapped to observed checks. + FeedbackLoopTraceability, /// Plan-level alignment with the user's intention was too low. Alignment, /// Plan-level understanding of the user's intent was too low. @@ -2194,7 +2196,8 @@ pub fn record_todo_gate(kind: TodoGateKind) { TodoGateKind::Ownership => &mut state.todo_gate_ownership_count, TodoGateKind::ClosedFeedbackLoop | TodoGateKind::FeedbackLoopRelevance - | TodoGateKind::FeedbackLoopCoverage => &mut state.todo_gate_feedback_loop_count, + | TodoGateKind::FeedbackLoopCoverage + | TodoGateKind::FeedbackLoopTraceability => &mut state.todo_gate_feedback_loop_count, TodoGateKind::Alignment => &mut state.todo_gate_alignment_count, TodoGateKind::IntentUnderstanding => &mut state.todo_gate_intent_count, TodoGateKind::Completion => &mut state.todo_gate_completion_count, @@ -2206,7 +2209,8 @@ pub fn record_todo_gate(kind: TodoGateKind) { TodoGateKind::Ownership => &mut turn.todo_gate_ownership_count, TodoGateKind::ClosedFeedbackLoop | TodoGateKind::FeedbackLoopRelevance - | TodoGateKind::FeedbackLoopCoverage => &mut turn.todo_gate_feedback_loop_count, + | TodoGateKind::FeedbackLoopCoverage + | TodoGateKind::FeedbackLoopTraceability => &mut turn.todo_gate_feedback_loop_count, TodoGateKind::Alignment => &mut turn.todo_gate_alignment_count, TodoGateKind::IntentUnderstanding => &mut turn.todo_gate_intent_count, TodoGateKind::Completion => &mut turn.todo_gate_completion_count, diff --git a/crates/jcode-tui/src/tui/app/todos_view.rs b/crates/jcode-tui/src/tui/app/todos_view.rs index a630c747ba..55e3b40fea 100644 --- a/crates/jcode-tui/src/tui/app/todos_view.rs +++ b/crates/jcode-tui/src/tui/app/todos_view.rs @@ -559,6 +559,12 @@ fn format_goal_markdown(goals: &[crate::todo::TodoGoal], group: Option<&str>) -> state.as_str() )); } + if let Some(state) = goal.feedback_loop_traceability { + line.push_str(&format!( + "- Feedback-loop traceability: **{}**\n", + state.as_str() + )); + } if let Some(state) = goal.delivery_state { line.push_str(&format!("- Delivery state: **{}**\n", state.as_str())); } @@ -764,6 +770,7 @@ fn hash_todos_payload( goal.feedback_loop.hash(&mut hasher); goal.feedback_loop_relevance.hash(&mut hasher); goal.feedback_loop_coverage.hash(&mut hasher); + goal.feedback_loop_traceability.hash(&mut hasher); goal.delivery_state.hash(&mut hasher); goal.difficulty.hash(&mut hasher); goal.autonomy.hash(&mut hasher); diff --git a/crates/jcode-tui/src/tui/info_widget_todos.rs b/crates/jcode-tui/src/tui/info_widget_todos.rs index b28b852d74..224b6719b7 100644 --- a/crates/jcode-tui/src/tui/info_widget_todos.rs +++ b/crates/jcode-tui/src/tui/info_widget_todos.rs @@ -140,6 +140,7 @@ fn push_goal_loop_suffix(spans: &mut Vec>, goal: &crate::todo::Tod if goal.closed_feedback_loop.is_none() && goal.feedback_loop_relevance.is_none() && goal.feedback_loop_coverage.is_none() + && goal.feedback_loop_traceability.is_none() { return; } @@ -156,6 +157,7 @@ fn push_goal_loop_suffix(spans: &mut Vec>, goal: &crate::todo::Tod for value in [ goal.feedback_loop_relevance.map(|state| state.as_str()), goal.feedback_loop_coverage.map(|state| state.as_str()), + goal.feedback_loop_traceability.map(|state| state.as_str()), ] .into_iter() .flatten() @@ -178,6 +180,7 @@ fn goal_loop_suffix_width(goal: &crate::todo::TodoGoal) -> u16 { goal.closed_feedback_loop.map(|state| state.as_str()), goal.feedback_loop_relevance.map(|state| state.as_str()), goal.feedback_loop_coverage.map(|state| state.as_str()), + goal.feedback_loop_traceability.map(|state| state.as_str()), ]; let values: Vec<&str> = states.into_iter().flatten().collect(); if values.is_empty() { diff --git a/crates/jcode-tui/src/tui/ui_messages.rs b/crates/jcode-tui/src/tui/ui_messages.rs index 3313f80ea3..6ab0c9c31f 100644 --- a/crates/jcode-tui/src/tui/ui_messages.rs +++ b/crates/jcode-tui/src/tui/ui_messages.rs @@ -1260,6 +1260,20 @@ fn todo_goal_score_spans(goal: &crate::todo::TodoGoal) -> Vec> { ); states.push(("Coverage", state, color)); } + if !crate::todo::feedback_loop_traceability_passes(goal) { + let (state, color) = goal.feedback_loop_traceability.map_or_else( + || ("missing".to_string(), todo_failure_color()), + |state| { + let color = if state == crate::todo::FeedbackLoopTraceability::Unmapped { + todo_failure_color() + } else { + todo_warning_color() + }; + (state.as_str().to_string(), color) + }, + ); + states.push(("Traceability", state, color)); + } if states.is_empty() { spans.push(Span::styled( @@ -1536,6 +1550,7 @@ fn push_todo_goal_details( goal.closed_feedback_loop, )) + usize::from(!crate::todo::feedback_loop_relevance_passes(goal)) + usize::from(!crate::todo::feedback_loop_coverage_passes(goal)) + + usize::from(!crate::todo::feedback_loop_traceability_passes(goal)) + usize::from(goal.delivery_state.is_some()); if score_width > inner_width.saturating_sub(2) && score_count > 1 { let mut states: Vec<(&str, String)> = Vec::new(); @@ -1566,6 +1581,15 @@ fn push_todo_goal_details( .to_string(), )); } + if !crate::todo::feedback_loop_traceability_passes(goal) { + states.push(( + "Traceability", + goal.feedback_loop_traceability + .map(|state| state.as_str()) + .unwrap_or("missing") + .to_string(), + )); + } if let Some(state) = goal.delivery_state { states.push(("Delivery", state.as_str().to_string())); } @@ -1585,7 +1609,7 @@ fn push_todo_goal_details( } } else if matches!( state.as_str(), - "missing" | "absent" | "weak" | "indirect" | "narrow" + "missing" | "absent" | "weak" | "indirect" | "narrow" | "unmapped" ) { todo_failure_color() } else { @@ -1773,6 +1797,22 @@ fn render_todo_goal_updates( base_indent, inner_width, ), + crate::todo::TodoGoalField::FeedbackLoopTraceability => push_todo_score_update( + &mut lines, + "Feedback-loop traceability", + update + .before + .as_ref() + .and_then(|goal| goal.feedback_loop_traceability) + .map(|state| state.as_str().to_string()), + update + .after + .as_ref() + .and_then(|goal| goal.feedback_loop_traceability) + .map(|state| state.as_str().to_string()), + base_indent, + inner_width, + ), crate::todo::TodoGoalField::DeliveryState => push_todo_score_update( &mut lines, "Delivery", From a4972b32a3e902264b7afd8c0069c72eb7740b35 Mon Sep 17 00:00:00 2001 From: jeremy <94247773+1jehuang@users.noreply.github.com> Date: Thu, 6 Aug 2026 02:04:24 -0700 Subject: [PATCH 13/45] fix: prevent empty transcript checkpoints (fixes #814) From 6dad509cca674ad7eec6cdd63c70c36bb1733603 Mon Sep 17 00:00:00 2001 From: jeremy <94247773+1jehuang@users.noreply.github.com> Date: Thu, 6 Aug 2026 02:06:51 -0700 Subject: [PATCH 14/45] log cargo action durations --- scripts/dev_cargo.sh | 75 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 73 insertions(+), 2 deletions(-) diff --git a/scripts/dev_cargo.sh b/scripts/dev_cargo.sh index d2e66c415d..32c171cdaa 100755 --- a/scripts/dev_cargo.sh +++ b/scripts/dev_cargo.sh @@ -18,6 +18,73 @@ log() { printf 'dev_cargo: %s\n' "$*" >&2 } +# Persist one record for every Cargo action routed through this wrapper. This is +# deliberately separate from session/tool history so compile and test latency +# can be inspected across sessions and after daemon restarts. +rust_action_log_started_ns="" +rust_action_log_started_at="" +rust_action_log_path="" +rust_action_log_execution="local" + +start_rust_action_log() { + case "${JCODE_RUST_ACTION_LOG:-1}" in + 0|false|no|off) return ;; + esac + + local state_root="${JCODE_HOME:-${HOME:+$HOME/.jcode}}" + [[ -n "$state_root" ]] || state_root="$repo_root/target/jcode-state" + rust_action_log_path="${JCODE_RUST_ACTION_LOG_PATH:-$state_root/logs/rust-actions.jsonl}" + rust_action_log_started_ns=$(date +%s%N) + rust_action_log_started_at=$(date -u +%Y-%m-%dT%H:%M:%S.%3NZ) + trap 'record_rust_action_log "$?"' EXIT +} + +record_rust_action_log() { + local exit_code="$1" + [[ -n "$rust_action_log_started_ns" && -n "$rust_action_log_path" ]] || return 0 + trap - EXIT + + local finished_ns duration_ms profile action + finished_ns=$(date +%s%N) + duration_ms=$(( (finished_ns - rust_action_log_started_ns) / 1000000 )) + profile=$(selected_profile "${cargo_argv[@]}") + action="${cargo_argv[0]:-unknown}" + mkdir -p "$(dirname "$rust_action_log_path")" 2>/dev/null || return 0 + + JCODE_LOG_STARTED_AT="$rust_action_log_started_at" \ + JCODE_LOG_DURATION_MS="$duration_ms" \ + JCODE_LOG_EXIT_CODE="$exit_code" \ + JCODE_LOG_ACTION="$action" \ + JCODE_LOG_PROFILE="$profile" \ + JCODE_LOG_REPO="$repo_root" \ + JCODE_LOG_EXECUTION="$rust_action_log_execution" \ + python3 - "$rust_action_log_path" "${cargo_argv[@]}" <<'PY' || true +import json +import os +import sys + +path = sys.argv[1] +record = { + "started_at": os.environ["JCODE_LOG_STARTED_AT"], + "duration_ms": int(os.environ["JCODE_LOG_DURATION_MS"]), + "exit_code": int(os.environ["JCODE_LOG_EXIT_CODE"]), + "success": os.environ["JCODE_LOG_EXIT_CODE"] == "0", + "action": os.environ["JCODE_LOG_ACTION"], + "profile": os.environ["JCODE_LOG_PROFILE"], + "repository": os.environ["JCODE_LOG_REPO"], + "execution": os.environ["JCODE_LOG_EXECUTION"], + "argv": sys.argv[2:], +} +line = (json.dumps(record, separators=(",", ":")) + "\n").encode() +fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_APPEND, 0o600) +try: + os.write(fd, line) +finally: + os.close(fd) +PY + return 0 +} + selected_linker_mode="not-configured" selected_linker_desc="" sccache_status="disabled" @@ -903,7 +970,7 @@ run_local_cargo() { return "$status" fi - exec cargo "${cargo_argv[@]}" + cargo "${cargo_argv[@]}" } validate_feature_profile @@ -928,10 +995,14 @@ while IFS= read -r -d '' arg; do cargo_argv+=("$arg") done < <(build_cargo_argv "$@") +start_rust_action_log + if [[ "${JCODE_REMOTE_CARGO:-0}" == "1" ]]; then if remote_cargo_preflight; then log "using remote cargo via scripts/remote_build.sh" - exec "$repo_root/scripts/remote_build.sh" "${cargo_argv[@]}" + rust_action_log_execution="remote" + "$repo_root/scripts/remote_build.sh" "${cargo_argv[@]}" + exit $? fi if [[ "$(remote_cargo_fallback_mode)" == "local" ]]; then log "remote cargo unavailable; falling back to local cargo (set JCODE_REMOTE_CARGO_FALLBACK=error to fail instead)" From 907e9a2ae07cbb6d4a0afa577357e7ef5e484128 Mon Sep 17 00:00:00 2001 From: jeremy <94247773+1jehuang@users.noreply.github.com> Date: Thu, 6 Aug 2026 02:13:34 -0700 Subject: [PATCH 15/45] fix(discovery): restore explicit select guidance --- crates/jcode-app-core/src/tool/discover.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/crates/jcode-app-core/src/tool/discover.rs b/crates/jcode-app-core/src/tool/discover.rs index 41a9da8adb..5fb2f3afb4 100644 --- a/crates/jcode-app-core/src/tool/discover.rs +++ b/crates/jcode-app-core/src/tool/discover.rs @@ -428,7 +428,7 @@ impl Tool for DiscoverToolsTool { "action": { "type": "string", "enum": ["search", "select", "suggest"], - "description": "Phase. Defaults to select when `tool` is set, else search. Select the product actually chosen, even when it is not in the catalog. Suggest a capability gap only when no product was chosen." + "description": "Phase. Defaults to select when `tool` is set, else search. For a listed result, select the one you commit to (it carries setup). Always select the product actually chosen, even when it is not in the catalog. Suggest a capability gap only when no product was chosen." }, "category": { "type": "string", @@ -1323,8 +1323,9 @@ fn render_listing(category: &str, listing: &Value, request_id: &str) -> Result Date: Thu, 6 Aug 2026 02:13:39 -0700 Subject: [PATCH 16/45] test(todo): cover traceability in TUI fixtures --- .../jcode-tui/src/tui/app/tests/remote_events_reload_05.rs | 5 +++++ crates/jcode-tui/src/tui/app/tests/state_model_poke_03.rs | 5 +++++ crates/jcode-tui/src/tui/ui_messages/tests.rs | 7 ++++++- 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/crates/jcode-tui/src/tui/app/tests/remote_events_reload_05.rs b/crates/jcode-tui/src/tui/app/tests/remote_events_reload_05.rs index 5232fa10fe..86737f7075 100644 --- a/crates/jcode-tui/src/tui/app/tests/remote_events_reload_05.rs +++ b/crates/jcode-tui/src/tui/app/tests/remote_events_reload_05.rs @@ -242,6 +242,11 @@ fn low_ownership_is_gated_after_the_completed_todo_was_saved() { delivery_state: Some(crate::todo::DeliveryState::Integrated), closed_feedback_loop: Some(crate::todo::FeedbackLoopState::from_legacy_score(100)), feedback_loop: Some("run the end-to-end release check".to_string()), + feedback_loop_relevance: Some(crate::todo::FeedbackLoopRelevance::Representative), + feedback_loop_coverage: Some(crate::todo::FeedbackLoopCoverage::MainPaths), + feedback_loop_traceability: Some(crate::todo::FeedbackLoopTraceability::Complete), + autonomy: Some(crate::todo::Autonomy::NecessaryFollowthrough), + iteration_maturity: Some(crate::todo::IterationMaturity::OutcomeReached), ..Default::default() }], ) diff --git a/crates/jcode-tui/src/tui/app/tests/state_model_poke_03.rs b/crates/jcode-tui/src/tui/app/tests/state_model_poke_03.rs index 72771cdffa..d890adb274 100644 --- a/crates/jcode-tui/src/tui/app/tests/state_model_poke_03.rs +++ b/crates/jcode-tui/src/tui/app/tests/state_model_poke_03.rs @@ -2486,6 +2486,11 @@ fn test_finish_turn_auto_poke_queues_confidence_summary_when_todos_done() { delivery_state: Some(crate::todo::DeliveryState::WorkflowValidated), autonomy: Some(crate::todo::Autonomy::NecessaryFollowthrough), iteration_maturity: Some(crate::todo::IterationMaturity::OutcomeReached), + closed_feedback_loop: Some(crate::todo::FeedbackLoopState::Closed), + feedback_loop: Some("verify completed work".to_string()), + feedback_loop_relevance: Some(crate::todo::FeedbackLoopRelevance::Representative), + feedback_loop_coverage: Some(crate::todo::FeedbackLoopCoverage::MainPaths), + feedback_loop_traceability: Some(crate::todo::FeedbackLoopTraceability::Complete), ..Default::default() }], ) diff --git a/crates/jcode-tui/src/tui/ui_messages/tests.rs b/crates/jcode-tui/src/tui/ui_messages/tests.rs index 0feb53d43a..71e4ad7b38 100644 --- a/crates/jcode-tui/src/tui/ui_messages/tests.rs +++ b/crates/jcode-tui/src/tui/ui_messages/tests.rs @@ -746,6 +746,7 @@ fn render_todos_message_collapses_passing_quality_gates() { closed_feedback_loop: Some(crate::todo::FeedbackLoopState::Closed), feedback_loop_relevance: Some(crate::todo::FeedbackLoopRelevance::AcceptanceAligned), feedback_loop_coverage: Some(crate::todo::FeedbackLoopCoverage::EdgeAndIntegrationPaths), + feedback_loop_traceability: Some(crate::todo::FeedbackLoopTraceability::Complete), delivery_state: Some(crate::todo::DeliveryState::OutcomeDelivered), ..Default::default() }]; @@ -1270,6 +1271,7 @@ fn visually_appealing_prompt_batched_retry_renders_complete_todo_card() { group: Some("pelican-bike".to_string()), closed_feedback_loop: Some(crate::todo::FeedbackLoopState::from_legacy_score(98)), feedback_loop: Some(FEEDBACK.to_string()), + feedback_loop_traceability: Some(crate::todo::FeedbackLoopTraceability::Complete), ..Default::default() }]; let plan = crate::todo::TodoPlan { @@ -1357,6 +1359,9 @@ fn render_ownership_gated_todo_result_keeps_the_full_card() { group: Some("ship outcome".to_string()), closed_feedback_loop: Some(crate::todo::FeedbackLoopState::from_legacy_score(100)), feedback_loop: Some("Run the complete workflow".to_string()), + feedback_loop_relevance: Some(crate::todo::FeedbackLoopRelevance::Representative), + feedback_loop_coverage: Some(crate::todo::FeedbackLoopCoverage::MainPaths), + feedback_loop_traceability: Some(crate::todo::FeedbackLoopTraceability::Complete), delivery_state: Some(crate::todo::DeliveryState::from_legacy_score(80)), ..Default::default() }]; @@ -1390,7 +1395,7 @@ fn render_ownership_gated_todo_result_keeps_the_full_card() { assert!(plain.contains("ship outcome ●"), "{plain}"); assert!(plain.contains("Deliver the complete workflow"), "{plain}"); assert!( - plain.contains("Closed feedback loop closed · Delivery workflow_validated"), + plain.contains("✓ All quality gates passing · Delivery workflow_validated"), "{plain}" ); assert!(!plain.contains("todo 1 items"), "{plain}"); From 8c0eec3da392305d8f69c888fcc8c624bcef4bbf Mon Sep 17 00:00:00 2001 From: jeremy <94247773+1jehuang@users.noreply.github.com> Date: Thu, 6 Aug 2026 02:14:39 -0700 Subject: [PATCH 17/45] route bash cargo commands through timing logger --- crates/jcode-app-core/src/tool/bash.rs | 38 ++++++++++++++++++++ crates/jcode-app-core/src/tool/bash_tests.rs | 25 +++++++++++++ 2 files changed, 63 insertions(+) diff --git a/crates/jcode-app-core/src/tool/bash.rs b/crates/jcode-app-core/src/tool/bash.rs index 384c5d8e69..b5f9431ef4 100644 --- a/crates/jcode-app-core/src/tool/bash.rs +++ b/crates/jcode-app-core/src/tool/bash.rs @@ -34,6 +34,38 @@ const BASH_TOOL_DESCRIPTION: &str = "Run a bash command."; const WINDOWS_SHELL_TOOL_DESCRIPTION: &str = "Run a Windows cmd.exe command (compatibility name `bash`). Use cmd.exe syntax, not Bash."; +#[cfg(unix)] +fn shell_single_quote(value: &str) -> String { + format!("'{}'", value.replace('\'', "'\"'\"'")) +} + +/// Route ordinary `cargo` invocations (including those inside child scripts) +/// through the repository wrapper. Besides applying the project's build policy, +/// that wrapper appends real action timings to rust-actions.jsonl. +#[cfg(unix)] +fn wrap_repo_cargo_commands(command: &str, working_dir: Option<&Path>) -> Option { + let working_dir = working_dir?; + let repo = crate::build::find_repo_in_ancestors(working_dir)?; + let wrapper = repo.join("scripts").join("dev_cargo.sh"); + if !wrapper.is_file() { + return None; + } + + Some(format!( + r#"export JCODE_DEV_CARGO_SCRIPT={wrapper} +cargo() {{ + if [[ "${{JCODE_IN_DEV_CARGO:-0}}" == "1" ]]; then + command cargo "$@" + else + JCODE_IN_DEV_CARGO=1 "$JCODE_DEV_CARGO_SCRIPT" "$@" + fi +}} +export -f cargo +{command}"#, + wrapper = shell_single_quote(&wrapper.to_string_lossy()), + )) +} + /// Build a clear timeout message. The `timeout` param is in milliseconds, which /// agents frequently mistake for seconds (e.g. passing 1000 thinking it means /// 1000s when it is 1s). Spell out the seconds equivalent and, for suspiciously @@ -700,6 +732,12 @@ impl Tool for BashTool { return Err(anyhow::anyhow!(refusal)); } + #[cfg(unix)] + if let Some(wrapped) = wrap_repo_cargo_commands(¶ms.command, ctx.working_dir.as_deref()) + { + params.command = wrapped; + } + if run_in_background { return self.execute_background(params, ctx).await; } diff --git a/crates/jcode-app-core/src/tool/bash_tests.rs b/crates/jcode-app-core/src/tool/bash_tests.rs index 948b84c353..1d361f5d69 100644 --- a/crates/jcode-app-core/src/tool/bash_tests.rs +++ b/crates/jcode-app-core/src/tool/bash_tests.rs @@ -5,6 +5,31 @@ use crate::tool::bash::{BashTool, parse_heuristic_progress}; use serde_json::json; use tokio::sync::mpsc; +#[test] +fn repository_commands_export_a_logged_cargo_function() { + let repo = + crate::build::find_repo_in_ancestors(std::path::Path::new(env!("CARGO_MANIFEST_DIR"))) + .expect("test runs inside the jcode repository"); + let wrapped = wrap_repo_cargo_commands("cargo test -p demo && echo done", Some(&repo)) + .expect("jcode repository has dev_cargo.sh"); + + assert!(wrapped.contains("export JCODE_DEV_CARGO_SCRIPT=")); + assert!(wrapped.contains("JCODE_IN_DEV_CARGO=1 \"$JCODE_DEV_CARGO_SCRIPT\" \"$@\"")); + assert!(wrapped.contains("export -f cargo")); + assert!(wrapped.ends_with("cargo test -p demo && echo done")); +} + +#[test] +fn cargo_routing_is_limited_to_the_jcode_repository() { + assert!(wrap_repo_cargo_commands("cargo test", Some(std::path::Path::new("/"))).is_none()); + assert!(wrap_repo_cargo_commands("cargo test", None).is_none()); +} + +#[test] +fn cargo_wrapper_path_is_shell_quoted() { + assert_eq!(shell_single_quote("a'b"), "'a'\"'\"'b'"); +} + fn make_ctx(stdin_tx: Option>) -> ToolContext { ToolContext { session_id: "test-session".to_string(), From fe83c27ba3a5eba5be817885ec26035e4f96073b Mon Sep 17 00:00:00 2001 From: jeremy <94247773+1jehuang@users.noreply.github.com> Date: Thu, 6 Aug 2026 02:19:31 -0700 Subject: [PATCH 18/45] fix: fall back when spawn hooks reject launch (fixes #792) --- crates/jcode-base/src/terminal_launch.rs | 45 ++++++++++++++++++++++-- 1 file changed, 43 insertions(+), 2 deletions(-) diff --git a/crates/jcode-base/src/terminal_launch.rs b/crates/jcode-base/src/terminal_launch.rs index f2c09dbc38..6cbab53fec 100644 --- a/crates/jcode-base/src/terminal_launch.rs +++ b/crates/jcode-base/src/terminal_launch.rs @@ -1,4 +1,4 @@ -use anyhow::Result; +use anyhow::{Result, bail}; pub use jcode_terminal_launch::{ SpawnAttempt, TerminalCommand, apply_client_terminal_env, build_hook_spawn_command, detected_resume_terminal, expand_home, parse_hook_command, resume_terminal_candidates, @@ -58,7 +58,26 @@ pub fn try_spawn_via_configured_hook(command: &TerminalCommand, cwd: &Path) -> b fn spawn_via_hook(hook: &str, command: &TerminalCommand, cwd: &Path) -> Result<()> { let mut cmd = build_hook_spawn_command(hook, command, cwd)?; - crate::platform::spawn_detached(&mut cmd)?; + let mut child = cmd.spawn()?; + let deadline = std::time::Instant::now() + std::time::Duration::from_secs(2); + loop { + if let Some(status) = child.try_wait()? { + if !status.success() { + bail!("hook exited with {status}"); + } + break; + } + if std::time::Instant::now() >= deadline { + // Long-running hooks may intentionally own their terminal process. + // Reap them asynchronously while treating survival past the startup + // window as successful launch admission. + std::thread::spawn(move || { + let _ = child.wait(); + }); + break; + } + std::thread::sleep(std::time::Duration::from_millis(10)); + } crate::logging::info(&format!( "Spawn hook '{hook}' launched terminal spawn (kind={:?} session={:?})", command.kind, command.session_id @@ -116,4 +135,26 @@ mod tests { "hook should receive metadata env and the jcode command as argv" ); } + + #[test] + fn spawn_via_hook_reports_immediate_nonzero_exit() { + use std::os::unix::fs::PermissionsExt; + + let temp = tempfile::TempDir::new().expect("temp dir"); + let hook_path = temp.path().join("reject-outside-tmux.sh"); + std::fs::write(&hook_path, "#!/bin/sh\nexit 1\n").expect("write hook"); + std::fs::set_permissions(&hook_path, std::fs::Permissions::from_mode(0o755)) + .expect("chmod hook"); + + let command = TerminalCommand::new( + "/usr/local/bin/jcode", + vec!["--resume".to_string(), "ses_fallback".to_string()], + ) + .kind("swarm-agent") + .session_id("ses_fallback"); + + let error = spawn_via_hook(&hook_path.to_string_lossy(), &command, temp.path()) + .expect_err("non-zero hook exit must trigger built-in/headless fallback"); + assert!(error.to_string().contains("hook exited with")); + } } From a4c00274e17b04a3836276fe8a1e19eadc6f8069 Mon Sep 17 00:00:00 2001 From: jeremy <94247773+1jehuang@users.noreply.github.com> Date: Thu, 6 Aug 2026 02:20:49 -0700 Subject: [PATCH 19/45] fix(ci): satisfy TUI quality guardrails --- crates/jcode-tui/src/tui/app/onboarding_graph.rs | 5 +++++ crates/jcode-tui/src/tui/app/turn_notify.rs | 6 ++++++ crates/jcode-tui/src/tui/ui_onboarding.rs | 2 +- 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/crates/jcode-tui/src/tui/app/onboarding_graph.rs b/crates/jcode-tui/src/tui/app/onboarding_graph.rs index 5a2cd04a6f..002470da8b 100644 --- a/crates/jcode-tui/src/tui/app/onboarding_graph.rs +++ b/crates/jcode-tui/src/tui/app/onboarding_graph.rs @@ -21,6 +21,11 @@ //! authored edges to prove the description stays faithful. That gives the //! anti-drift guarantee without a risky rewrite of the running flow. +// Most of this descriptive graph is exercised by the exhaustive tests below. +// The live flow only needs the node vocabulary, so production builds naturally +// leave the invariant-checking helpers unused. +#![cfg_attr(not(test), allow(dead_code))] + use std::collections::{BTreeMap, BTreeSet}; /// A node in the onboarding graph. diff --git a/crates/jcode-tui/src/tui/app/turn_notify.rs b/crates/jcode-tui/src/tui/app/turn_notify.rs index e5bc8b7c28..740fcd2c88 100644 --- a/crates/jcode-tui/src/tui/app/turn_notify.rs +++ b/crates/jcode-tui/src/tui/app/turn_notify.rs @@ -8,6 +8,7 @@ use super::App; use crate::todo::TodoItem; +#[cfg(any(target_os = "macos", test))] use base64::Engine as _; #[cfg(target_os = "macos")] use std::io::Write; @@ -151,6 +152,7 @@ fn send_originating_terminal_notification( false } +#[cfg(any(target_os = "macos", test))] fn notification_text(notification: &TurnNotification) -> String { match notification.subtitle.as_deref() { Some(subtitle) => format!("{}\n{}", subtitle, notification.body), @@ -158,10 +160,12 @@ fn notification_text(notification: &TurnNotification) -> String { } } +#[cfg(any(target_os = "macos", test))] fn osc_safe(text: &str) -> String { text.chars().filter(|ch| !ch.is_control()).collect() } +#[cfg(any(target_os = "macos", test))] fn kitty_notification_id(session_id: &str) -> String { let safe: String = session_id .chars() @@ -174,6 +178,7 @@ fn kitty_notification_id(session_id: &str) -> String { ) } +#[cfg(any(target_os = "macos", test))] fn kitty_notification_sequence(notification: &TurnNotification, session_id: &str) -> String { // OSC 99 is Kitty's desktop-notification protocol. Notifications are tied // to the originating Kitty window, which is what makes click-to-focus work. @@ -191,6 +196,7 @@ fn kitty_notification_sequence(notification: &TurnNotification, session_id: &str ) } +#[cfg(any(target_os = "macos", test))] fn iterm_notification_sequence(notification: &TurnNotification) -> String { // iTerm2's OSC 9 notification is likewise associated with its source tab. let text = osc_safe(&format!( diff --git a/crates/jcode-tui/src/tui/ui_onboarding.rs b/crates/jcode-tui/src/tui/ui_onboarding.rs index 459eb0e174..dde39085d5 100644 --- a/crates/jcode-tui/src/tui/ui_onboarding.rs +++ b/crates/jcode-tui/src/tui/ui_onboarding.rs @@ -684,7 +684,7 @@ pub(super) fn draw_onboarding_welcome(frame: &mut Frame, app: &dyn TuiState, are // The title/hint block is dropped first when the area is short so the // phase body (the actionable part) always fits. - let show_title_block = area.height >= telemetry_h + TITLE_H + HINT_H + body_h + GAP * 2 + 1; + let show_title_block = area.height > telemetry_h + TITLE_H + HINT_H + body_h + GAP * 2; let used = if show_title_block { telemetry_h + GAP + TITLE_H + HINT_H + GAP + body_h From fb97e18efb8333f3e208c0a292eb48cb30d2ff85 Mon Sep 17 00:00:00 2001 From: jeremy <94247773+1jehuang@users.noreply.github.com> Date: Thu, 6 Aug 2026 02:22:01 -0700 Subject: [PATCH 20/45] fix(schedule): keep scheduled turns out of user prompt history --- .../src/agent/turn_execution.rs | 43 +++++++++++++-- crates/jcode-app-core/src/ambient/runner.rs | 14 ++++- .../src/server/client_actions.rs | 4 +- .../src/server/client_actions_tests.rs | 1 + crates/jcode-app-core/src/server/live_turn.rs | 54 ++++++++++++++++--- crates/jcode-base/src/session.rs | 14 ++++- crates/jcode-base/src/session/render.rs | 1 + crates/jcode-base/src/session_tests/cases.rs | 17 ++++++ .../src/tui/app/state_ui_messages.rs | 3 ++ 9 files changed, 135 insertions(+), 16 deletions(-) diff --git a/crates/jcode-app-core/src/agent/turn_execution.rs b/crates/jcode-app-core/src/agent/turn_execution.rs index 5adf8cc152..aa4a51d8c4 100644 --- a/crates/jcode-app-core/src/agent/turn_execution.rs +++ b/crates/jcode-app-core/src/agent/turn_execution.rs @@ -20,12 +20,22 @@ impl Agent { } pub async fn run_once_capture(&mut self, user_message: &str) -> Result { - self.add_message( + self.run_once_capture_with_display_role(user_message, None) + .await + } + + pub(crate) async fn run_once_capture_with_display_role( + &mut self, + user_message: &str, + display_role: Option, + ) -> Result { + self.add_message_with_display_role( Role::User, vec![ContentBlock::Text { text: user_message.to_string(), cache_control: None, }], + display_role, ); self.session.save()?; if trace_enabled() { @@ -41,6 +51,24 @@ impl Agent { images: Vec<(String, String)>, system_reminder: Option, event_tx: mpsc::UnboundedSender, + ) -> Result<()> { + self.run_once_streaming_mpsc_with_display_role( + user_message, + images, + system_reminder, + event_tx, + None, + ) + .await + } + + pub(crate) async fn run_once_streaming_mpsc_with_display_role( + &mut self, + user_message: &str, + images: Vec<(String, String)>, + system_reminder: Option, + event_tx: mpsc::UnboundedSender, + display_role: Option, ) -> Result<()> { // Inject any pending notifications before the user message let alerts = self.take_alerts(); @@ -62,7 +90,7 @@ impl Agent { self.current_turn_system_reminder = system_reminder.filter(|value| !value.trim().is_empty()); - self.append_user_context_message(user_message, images)?; + self.append_user_context_message_with_display_role(user_message, images, display_role)?; crate::telemetry::record_turn(); let turn_started_at = Instant::now(); let start_message_index = self.message_count(); @@ -78,6 +106,15 @@ impl Agent { &mut self, user_message: &str, images: Vec<(String, String)>, + ) -> Result<()> { + self.append_user_context_message_with_display_role(user_message, images, None) + } + + fn append_user_context_message_with_display_role( + &mut self, + user_message: &str, + images: Vec<(String, String)>, + display_role: Option, ) -> Result<()> { let mut blocks: Vec = images .into_iter() @@ -95,7 +132,7 @@ impl Agent { )); } - self.add_message(Role::User, blocks); + self.add_message_with_display_role(Role::User, blocks, display_role); self.session.save() } diff --git a/crates/jcode-app-core/src/ambient/runner.rs b/crates/jcode-app-core/src/ambient/runner.rs index fd5708642c..4ad30adeb7 100644 --- a/crates/jcode-app-core/src/ambient/runner.rs +++ b/crates/jcode-app-core/src/ambient/runner.rs @@ -396,7 +396,12 @@ impl AmbientRunnerHandle { agent.restore_session(session_id)?; let reminder = ambient::format_scheduled_session_message(item); - let _ = agent.run_once_capture(&reminder).await?; + let _ = agent + .run_once_capture_with_display_role( + &reminder, + Some(crate::session::StoredDisplayRole::System), + ) + .await?; agent.mark_closed(); Ok(()) } @@ -470,7 +475,12 @@ impl AmbientRunnerHandle { } let reminder = ambient::format_scheduled_session_message(item); - let _ = agent.run_once_capture(&reminder).await?; + let _ = agent + .run_once_capture_with_display_role( + &reminder, + Some(crate::session::StoredDisplayRole::System), + ) + .await?; agent.mark_closed(); Ok(child_session_id) } diff --git a/crates/jcode-app-core/src/server/client_actions.rs b/crates/jcode-app-core/src/server/client_actions.rs index 67dba99278..e0c86a33ae 100644 --- a/crates/jcode-app-core/src/server/client_actions.rs +++ b/crates/jcode-app-core/src/server/client_actions.rs @@ -105,10 +105,9 @@ pub(super) async fn handle_notify_session( }; let ran_immediately = if target_has_client { - super::live_turn::run_live_turn_if_idle( + super::live_turn::run_live_system_turn_if_idle( &session_id, &message, - None, ctx.sessions, super::live_turn::LiveTurnSwarmContext::new( ctx.swarm_members, @@ -992,6 +991,7 @@ pub(super) async fn handle_resume_all_sessions( Arc::clone(&agent), String::new(), Some(reminder), + None, Some("resuming interrupted session".to_string()), super::live_turn::LiveTurnSwarmContext::new( swarm_members, diff --git a/crates/jcode-app-core/src/server/client_actions_tests.rs b/crates/jcode-app-core/src/server/client_actions_tests.rs index 6100880d98..6f438827b1 100644 --- a/crates/jcode-app-core/src/server/client_actions_tests.rs +++ b/crates/jcode-app-core/src/server/client_actions_tests.rs @@ -461,6 +461,7 @@ async fn notify_session_runs_scheduled_task_immediately_for_idle_live_session() let guard = agent.lock().await; assert!(guard.messages().iter().any(|message| { message.role == Role::User + && message.display_role == Some(crate::session::StoredDisplayRole::System) && message .content_preview() .contains("[Scheduled task] Task: Follow up") diff --git a/crates/jcode-app-core/src/server/live_turn.rs b/crates/jcode-app-core/src/server/live_turn.rs index 5604ef1a1d..667d6842c3 100644 --- a/crates/jcode-app-core/src/server/live_turn.rs +++ b/crates/jcode-app-core/src/server/live_turn.rs @@ -95,6 +95,7 @@ pub(super) async fn spawn_tracked_live_turn( agent: Arc>, message: String, system_reminder: Option, + display_role: Option, status_detail: Option, swarm: LiveTurnSwarmContext, ) { @@ -117,14 +118,27 @@ pub(super) async fn spawn_tracked_live_turn( let agent_guard = agent.lock().await; agent_guard.message_count() }; - let result = process_message_streaming_mpsc( - Arc::clone(&agent), - &message, - vec![], - system_reminder, - event_tx.clone(), - ) - .await; + let result = if let Some(display_role) = display_role { + let mut agent = agent.lock().await; + agent + .run_once_streaming_mpsc_with_display_role( + &message, + vec![], + system_reminder, + event_tx.clone(), + Some(display_role), + ) + .await + } else { + process_message_streaming_mpsc( + Arc::clone(&agent), + &message, + vec![], + system_reminder, + event_tx.clone(), + ) + .await + }; match result { Ok(()) => { let completion_report = { @@ -189,6 +203,30 @@ pub(super) async fn run_live_turn_if_idle( agent, message.to_string(), system_reminder, + None, + detail, + swarm, + ) + .await; + true +} + +pub(super) async fn run_live_system_turn_if_idle( + session_id: &str, + message: &str, + sessions: &SessionAgents, + swarm: LiveTurnSwarmContext, +) -> bool { + let Some(agent) = idle_live_agent(session_id, sessions, &swarm.members).await else { + return false; + }; + let detail = Some(truncate_detail(message, 120)).filter(|detail| !detail.is_empty()); + spawn_tracked_live_turn( + session_id, + agent, + message.to_string(), + None, + Some(crate::session::StoredDisplayRole::System), detail, swarm, ) diff --git a/crates/jcode-base/src/session.rs b/crates/jcode-base/src/session.rs index bf652cc8b6..387ebe4e62 100644 --- a/crates/jcode-base/src/session.rs +++ b/crates/jcode-base/src/session.rs @@ -86,7 +86,19 @@ fn is_internal_system_reminder_message(message: &StoredMessage) -> bool { } fn is_visible_conversation_message(message: &StoredMessage) -> bool { - message.display_role.is_none() && !is_internal_system_reminder_message(message) + message.display_role.is_none() + && !is_internal_system_reminder_message(message) + && !is_scheduled_task_message(message) +} + +/// Recognize scheduler prompts persisted before they received an explicit +/// system display role. This keeps old sessions from treating them as user +/// prompts after resume. +pub fn is_scheduled_task_message(message: &StoredMessage) -> bool { + message.role == Role::User + && message.content.iter().any(|block| { + matches!(block, ContentBlock::Text { text, .. } if text.trim_start().starts_with("[Scheduled task]\n")) + }) } #[derive(Debug, Clone, Serialize, Deserialize)] diff --git a/crates/jcode-base/src/session/render.rs b/crates/jcode-base/src/session/render.rs index b1697c9384..147e981d79 100644 --- a/crates/jcode-base/src/session/render.rs +++ b/crates/jcode-base/src/session/render.rs @@ -412,6 +412,7 @@ pub fn render_messages_and_images_with_compacted_history( Some(StoredDisplayRole::System) => "system", Some(StoredDisplayRole::BackgroundTask) => "background_task", None if is_auto_poke_user_message(msg) => "system", + None if super::is_scheduled_task_message(msg) => "system", None => match msg.role { Role::User => "user", Role::Assistant => "assistant", diff --git a/crates/jcode-base/src/session_tests/cases.rs b/crates/jcode-base/src/session_tests/cases.rs index 0798e1cd1a..7c0126bd8c 100644 --- a/crates/jcode-base/src/session_tests/cases.rs +++ b/crates/jcode-base/src/session_tests/cases.rs @@ -1289,6 +1289,23 @@ fn test_render_messages_honors_system_display_role_override() { assert!(rendered[0].content.contains("Background Task Completed")); } +#[test] +fn legacy_scheduled_task_message_renders_as_system() { + let mut session = Session::create(None, None); + session.add_message( + Role::User, + vec![ContentBlock::Text { + text: "[Scheduled task]\nA scheduled task for this session is now due.\n\nTask: check progress".to_string(), + cache_control: None, + }], + ); + + let rendered = render::render_messages(&session); + assert_eq!(rendered.len(), 1); + assert_eq!(rendered[0].role, "system"); + assert_eq!(session.visible_conversation_message_count(), 0); +} + #[test] fn test_render_messages_shows_auto_poke_continuations_as_system_not_user() { // Regression: incomplete-todo and private-quality continuations are persisted as diff --git a/crates/jcode-tui/src/tui/app/state_ui_messages.rs b/crates/jcode-tui/src/tui/app/state_ui_messages.rs index 66ac6abf81..72f2877031 100644 --- a/crates/jcode-tui/src/tui/app/state_ui_messages.rs +++ b/crates/jcode-tui/src/tui/app/state_ui_messages.rs @@ -24,6 +24,9 @@ fn display_message_from_stored_message( } None => match message.role { Role::User => { + if crate::session::is_scheduled_task_message(message) { + return Some(DisplayMessage::system(text)); + } // Synthetic auto-poke continuations are persisted as user // turns for the model but must not display as user prompts. if crate::todo::is_auto_poke_message(&text) { From 33559060de5c41516d803a56b2861f20ff07f089 Mon Sep 17 00:00:00 2001 From: jeremy <94247773+1jehuang@users.noreply.github.com> Date: Thu, 6 Aug 2026 02:22:55 -0700 Subject: [PATCH 21/45] test(todo): align TUI gate fixtures --- crates/jcode-tui/src/tui/app/tests/remote_events_reload_05.rs | 2 +- crates/jcode-tui/src/tui/app/tests/state_model_poke_03.rs | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/jcode-tui/src/tui/app/tests/remote_events_reload_05.rs b/crates/jcode-tui/src/tui/app/tests/remote_events_reload_05.rs index 86737f7075..603367f163 100644 --- a/crates/jcode-tui/src/tui/app/tests/remote_events_reload_05.rs +++ b/crates/jcode-tui/src/tui/app/tests/remote_events_reload_05.rs @@ -255,7 +255,7 @@ fn low_ownership_is_gated_after_the_completed_todo_was_saved() { assert!(app.schedule_auto_poke_followup_if_needed()); assert!(app.pending_queued_dispatch); assert_eq!(app.queued_messages.len(), 1); - assert!(app.queued_messages[0].contains("delivery_state")); + assert!(app.queued_messages[0].contains("complete workflow")); let saved = crate::todo::load_todos(&app.session.id).expect("load saved todo"); assert_eq!(saved[0].status, "completed"); diff --git a/crates/jcode-tui/src/tui/app/tests/state_model_poke_03.rs b/crates/jcode-tui/src/tui/app/tests/state_model_poke_03.rs index d890adb274..50119b2c76 100644 --- a/crates/jcode-tui/src/tui/app/tests/state_model_poke_03.rs +++ b/crates/jcode-tui/src/tui/app/tests/state_model_poke_03.rs @@ -2508,7 +2508,6 @@ fn test_finish_turn_auto_poke_queues_confidence_summary_when_todos_done() { assert!(super::commands::is_poke_message(summary)); assert!(super::commands::is_todo_confidence_summary_message(summary)); assert!(summary.starts_with(crate::todo::TODO_COMPLETION_CONTINUATION_MESSAGE)); - assert!(summary.contains("completion confidence")); // The continuation self-identifies as an automated gate so the model // does not mistake it for a user message, but never discloses the // numeric threshold. From 25927cc16086ed2fb896c2464d5eb145197f947a Mon Sep 17 00:00:00 2001 From: jeremy <94247773+1jehuang@users.noreply.github.com> Date: Thu, 6 Aug 2026 02:26:24 -0700 Subject: [PATCH 22/45] test(todo): expect generic completion follow-up wording --- crates/jcode-tui/src/tui/app/tests/state_model_poke_03.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/jcode-tui/src/tui/app/tests/state_model_poke_03.rs b/crates/jcode-tui/src/tui/app/tests/state_model_poke_03.rs index 50119b2c76..5933511b7e 100644 --- a/crates/jcode-tui/src/tui/app/tests/state_model_poke_03.rs +++ b/crates/jcode-tui/src/tui/app/tests/state_model_poke_03.rs @@ -2508,10 +2508,10 @@ fn test_finish_turn_auto_poke_queues_confidence_summary_when_todos_done() { assert!(super::commands::is_poke_message(summary)); assert!(super::commands::is_todo_confidence_summary_message(summary)); assert!(summary.starts_with(crate::todo::TODO_COMPLETION_CONTINUATION_MESSAGE)); - // The continuation self-identifies as an automated gate so the model - // does not mistake it for a user message, but never discloses the - // numeric threshold. - assert!(summary.contains("automated todo completion gate")); + // The continuation self-identifies as an automated follow-up so the model + // does not mistake it for a user message, but never discloses private + // calibration details. + assert!(summary.contains("automated follow-up")); assert!(!summary.to_ascii_lowercase().contains("threshold")); // The model is told exactly which completed todos to recheck. assert!(summary.contains("Finish risky provider path")); From df686c78cdaae003e6becfb83fb258837dd2f92c Mon Sep 17 00:00:00 2001 From: jeremy <94247773+1jehuang@users.noreply.github.com> Date: Thu, 6 Aug 2026 02:27:23 -0700 Subject: [PATCH 23/45] test(todo): assert compact batched card contract --- crates/jcode-tui/src/tui/ui_messages/tests.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/crates/jcode-tui/src/tui/ui_messages/tests.rs b/crates/jcode-tui/src/tui/ui_messages/tests.rs index 71e4ad7b38..96efe4e9c0 100644 --- a/crates/jcode-tui/src/tui/ui_messages/tests.rs +++ b/crates/jcode-tui/src/tui/ui_messages/tests.rs @@ -1329,10 +1329,11 @@ fn visually_appealing_prompt_batched_retry_renders_complete_todo_card() { compact.contains(&without_whitespace(OBJECTIVE)), "batched todo plan intention was truncated:\n{rendered}" ); - assert!( - compact.contains(&without_whitespace(FEEDBACK)), - "batched todo feedback loop was truncated:\n{rendered}" - ); + // Compact transcript cards show the goal's quality assessments rather than + // repeating its potentially long feedback-loop prose. The full prose remains + // available in the serialized todo payload and the todos side panel. + assert!(rendered.contains("Relevance missing · Coverage missing")); + assert!(!compact.contains(&without_whitespace(FEEDBACK))); let goal_details = rendered .split_once("pelican-bike") .map(|(_, details)| details) From ab19a8129d1332ae2cc911162af7f30610844a55 Mon Sep 17 00:00:00 2001 From: jeremy <94247773+1jehuang@users.noreply.github.com> Date: Thu, 6 Aug 2026 02:28:47 -0700 Subject: [PATCH 24/45] test(todo): cover compact narrow card rendering --- crates/jcode-tui/src/tui/ui_messages/tests.rs | 25 ++++--------------- 1 file changed, 5 insertions(+), 20 deletions(-) diff --git a/crates/jcode-tui/src/tui/ui_messages/tests.rs b/crates/jcode-tui/src/tui/ui_messages/tests.rs index 96efe4e9c0..5afaa3c34f 100644 --- a/crates/jcode-tui/src/tui/ui_messages/tests.rs +++ b/crates/jcode-tui/src/tui/ui_messages/tests.rs @@ -1205,10 +1205,8 @@ fn unbiased_visual_prompt_retry_renders_complete_feedback_change() { }), ); assert!(initial.contains("pelican-bike-animation"), "{initial}"); - assert!( - without_whitespace(&initial).contains(&without_whitespace(INITIAL_FEEDBACK)), - "initial feedback loop was truncated:\n{initial}" - ); + assert!(initial.contains("Closed feedback loop strong"), "{initial}"); + assert!(!without_whitespace(&initial).contains(&without_whitespace(INITIAL_FEEDBACK))); // Simulate a restored/mirrored result whose ToolCall association was lost. // The structured result must still render as the same complete todo card. @@ -1225,22 +1223,9 @@ fn unbiased_visual_prompt_retry_renders_complete_feedback_change() { ); let compact_revised = without_whitespace(&revised); assert!(revised.contains("pelican-bike-animation"), "{revised}"); - assert!( - compact_revised.contains(&without_whitespace(REVISED_OBJECTIVE)), - "revised plan intention was truncated:\n{revised}" - ); - assert!( - compact_revised.contains(&without_whitespace(REVISED_FEEDBACK)), - "revised feedback loop was truncated:\n{revised}" - ); - let goal_details = revised - .split("● Implement") - .next() - .expect("todo item should follow the goal details"); - assert!( - !goal_details.contains('…'), - "todo goal details must not truncate:\n{revised}" - ); + assert!(revised.contains("Closed feedback loop closed"), "{revised}"); + assert!(!compact_revised.contains(&without_whitespace(REVISED_FEEDBACK))); + assert!(revised.contains("● Implement"), "{revised}"); } #[test] From 247886aa74b511993f4991ca3117048f27029ab4 Mon Sep 17 00:00:00 2001 From: jeremy <94247773+1jehuang@users.noreply.github.com> Date: Thu, 6 Aug 2026 02:28:54 -0700 Subject: [PATCH 25/45] chore(ci): refresh quality ratchets --- scripts/code_size_budget.json | 111 ++++++----- scripts/panic_budget.json | 10 +- scripts/swallowed_error_budget.json | 255 ++++++++++++++++++++------ scripts/test_size_budget.json | 42 +++-- scripts/wildcard_reexport_budget.json | 3 +- 5 files changed, 289 insertions(+), 132 deletions(-) diff --git a/scripts/code_size_budget.json b/scripts/code_size_budget.json index c7cdf68c90..64e12c0388 100644 --- a/scripts/code_size_budget.json +++ b/scripts/code_size_budget.json @@ -1,26 +1,29 @@ { "threshold_loc": 1200, "tracked_files": { - "crates/jcode-app-core/src/agent/turn_streaming_mpsc.rs": 1710, + "crates/jcode-app-core/src/agent/turn_loops.rs": 1260, + "crates/jcode-app-core/src/agent/turn_streaming_mpsc.rs": 1730, "crates/jcode-app-core/src/overnight.rs": 1275, "crates/jcode-app-core/src/server.rs": 2376, - "crates/jcode-app-core/src/server/client_lifecycle.rs": 3180, - "crates/jcode-app-core/src/server/client_session.rs": 1710, + "crates/jcode-app-core/src/server/client_lifecycle.rs": 3282, + "crates/jcode-app-core/src/server/client_session.rs": 1716, "crates/jcode-app-core/src/server/comm_control.rs": 2625, - "crates/jcode-app-core/src/server/comm_session.rs": 1435, + "crates/jcode-app-core/src/server/comm_session.rs": 1434, "crates/jcode-app-core/src/server/debug_server_state.rs": 1257, "crates/jcode-app-core/src/server/jade_relay.rs": 1429, "crates/jcode-app-core/src/server/provider_control.rs": 1600, "crates/jcode-app-core/src/server/swarm.rs": 3170, - "crates/jcode-app-core/src/tool/bash.rs": 1283, + "crates/jcode-app-core/src/tool/bash.rs": 1321, "crates/jcode-app-core/src/tool/communicate.rs": 3351, - "crates/jcode-app-core/src/tool/discover.rs": 2091, + "crates/jcode-app-core/src/tool/discover.rs": 2300, + "crates/jcode-app-core/src/tool/mod.rs": 1230, + "crates/jcode-app-core/src/tool/selfdev/build_queue.rs": 1264, "crates/jcode-app-core/src/tool/session_search.rs": 1892, - "crates/jcode-app-core/src/tool/todo.rs": 1804, + "crates/jcode-app-core/src/tool/todo.rs": 2472, "crates/jcode-app-core/src/update.rs": 1717, "crates/jcode-base/src/auth/lifecycle.rs": 2593, - "crates/jcode-base/src/auth/mod.rs": 1561, - "crates/jcode-base/src/auth/oauth.rs": 1531, + "crates/jcode-base/src/auth/mod.rs": 1615, + "crates/jcode-base/src/auth/oauth.rs": 1518, "crates/jcode-base/src/background.rs": 1465, "crates/jcode-base/src/compaction.rs": 1790, "crates/jcode-base/src/gmail.rs": 1213, @@ -28,80 +31,88 @@ "crates/jcode-base/src/memory.rs": 2065, "crates/jcode-base/src/memory_agent.rs": 1901, "crates/jcode-base/src/provider/catalog_routes.rs": 1610, - "crates/jcode-base/src/provider/mod.rs": 2798, - "crates/jcode-base/src/session.rs": 1622, - "crates/jcode-base/src/sidecar.rs": 1319, + "crates/jcode-base/src/provider/mod.rs": 2805, + "crates/jcode-base/src/session.rs": 1634, + "crates/jcode-base/src/sidecar.rs": 1438, "crates/jcode-base/src/skill.rs": 1426, - "crates/jcode-base/src/todo.rs": 1277, - "crates/jcode-config-types/src/lib.rs": 1496, + "crates/jcode-base/src/todo.rs": 2007, + "crates/jcode-config-types/src/lib.rs": 1549, "crates/jcode-desktop2/src/editor.rs": 1373, - "crates/jcode-desktop2/src/main.rs": 1667, - "crates/jcode-desktop2/src/scene.rs": 1256, - "crates/jcode-desktop2/src/states.rs": 1243, - "crates/jcode-desktop2/src/transcript.rs": 3213, + "crates/jcode-desktop2/src/keymap.rs": 1323, + "crates/jcode-desktop2/src/layout.rs": 1453, + "crates/jcode-desktop2/src/main.rs": 2207, + "crates/jcode-desktop2/src/scene.rs": 1900, + "crates/jcode-desktop2/src/states.rs": 1593, + "crates/jcode-desktop2/src/transcript.rs": 3974, + "crates/jcode-harness-api-server/src/translate.rs": 1851, "crates/jcode-import-core/src/lib.rs": 1645, + "crates/jcode-math/src/layout.rs": 1512, "crates/jcode-plan/src/lib.rs": 1201, - "crates/jcode-protocol/src/wire.rs": 1450, - "crates/jcode-provider-anthropic-runtime/src/lib.rs": 2394, + "crates/jcode-protocol/src/wire.rs": 1460, + "crates/jcode-provider-anthropic-runtime/src/lib.rs": 2499, "crates/jcode-provider-bedrock/src/lib.rs": 1979, "crates/jcode-provider-core/src/lib.rs": 1642, "crates/jcode-provider-doctor/src/lifecycle_driver.rs": 1974, - "crates/jcode-provider-doctor/src/live_provider_probes.rs": 2029, + "crates/jcode-provider-doctor/src/live_provider_probes.rs": 2031, "crates/jcode-provider-doctor/src/provider_e2e.rs": 2713, "crates/jcode-provider-openai-runtime/src/lib.rs": 1384, - "crates/jcode-provider-openai-runtime/src/openai_provider_impl.rs": 1208, + "crates/jcode-provider-openai-runtime/src/openai_provider_impl.rs": 1229, "crates/jcode-provider-openai-runtime/src/openai_stream_runtime.rs": 1650, - "crates/jcode-provider-openrouter-runtime/src/lib.rs": 2821, + "crates/jcode-provider-openrouter-runtime/src/lib.rs": 2707, "crates/jcode-render-core/src/math.rs": 1234, + "crates/jcode-sdk/src/client.rs": 1379, "crates/jcode-setup-hints/src/lib.rs": 2635, - "crates/jcode-telemetry-core/src/lib.rs": 2165, - "crates/jcode-terminal-launch/src/lib.rs": 1219, - "crates/jcode-tui-mermaid/src/lib.rs": 1496, - "crates/jcode-tui-mermaid/src/mermaid_cache_render.rs": 1467, + "crates/jcode-telemetry-core/src/lib.rs": 2340, + "crates/jcode-terminal-launch/src/lib.rs": 1692, + "crates/jcode-tui-markdown/src/markdown_latex_image.rs": 1294, + "crates/jcode-tui-mermaid/src/lib.rs": 1497, + "crates/jcode-tui-mermaid/src/mermaid_cache_render.rs": 1475, "crates/jcode-tui-mermaid/src/mermaid_viewport.rs": 1953, "crates/jcode-tui-render/src/swarm_gallery.rs": 3099, - "crates/jcode-tui/src/tui/app.rs": 2528, - "crates/jcode-tui/src/tui/app/auth.rs": 3391, + "crates/jcode-tui/src/tui/app.rs": 2542, + "crates/jcode-tui/src/tui/app/auth.rs": 3433, + "crates/jcode-tui/src/tui/app/auth_account_commands.rs": 1202, "crates/jcode-tui/src/tui/app/auth_account_picker.rs": 1220, - "crates/jcode-tui/src/tui/app/commands.rs": 3492, + "crates/jcode-tui/src/tui/app/commands.rs": 3537, "crates/jcode-tui/src/tui/app/debug_bench.rs": 1284, - "crates/jcode-tui/src/tui/app/helpers.rs": 1506, - "crates/jcode-tui/src/tui/app/inline_interactive.rs": 4209, - "crates/jcode-tui/src/tui/app/input.rs": 3905, + "crates/jcode-tui/src/tui/app/helpers.rs": 1502, + "crates/jcode-tui/src/tui/app/inline_interactive.rs": 4334, + "crates/jcode-tui/src/tui/app/input.rs": 4022, "crates/jcode-tui/src/tui/app/model_context.rs": 1945, - "crates/jcode-tui/src/tui/app/navigation.rs": 1835, - "crates/jcode-tui/src/tui/app/onboarding_flow_control.rs": 1696, - "crates/jcode-tui/src/tui/app/remote.rs": 2073, - "crates/jcode-tui/src/tui/app/remote/key_handling.rs": 2615, + "crates/jcode-tui/src/tui/app/navigation.rs": 1843, + "crates/jcode-tui/src/tui/app/onboarding_flow_control.rs": 1743, + "crates/jcode-tui/src/tui/app/remote.rs": 2079, + "crates/jcode-tui/src/tui/app/remote/key_handling.rs": 2635, "crates/jcode-tui/src/tui/app/remote/server_events.rs": 2832, "crates/jcode-tui/src/tui/app/run_shell.rs": 1329, "crates/jcode-tui/src/tui/app/state_ui.rs": 2211, - "crates/jcode-tui/src/tui/app/state_ui_input_helpers.rs": 2081, - "crates/jcode-tui/src/tui/app/tui_lifecycle.rs": 1336, - "crates/jcode-tui/src/tui/app/tui_state.rs": 2413, + "crates/jcode-tui/src/tui/app/state_ui_input_helpers.rs": 2085, + "crates/jcode-tui/src/tui/app/tui_lifecycle.rs": 1349, + "crates/jcode-tui/src/tui/app/tui_state.rs": 2417, "crates/jcode-tui/src/tui/app/turn.rs": 1485, - "crates/jcode-tui/src/tui/backend.rs": 1820, + "crates/jcode-tui/src/tui/backend.rs": 1863, "crates/jcode-tui/src/tui/info_widget.rs": 2233, - "crates/jcode-tui/src/tui/mod.rs": 1869, + "crates/jcode-tui/src/tui/mod.rs": 1879, "crates/jcode-tui/src/tui/session_picker.rs": 2437, "crates/jcode-tui/src/tui/session_picker/loading.rs": 2983, - "crates/jcode-tui/src/tui/ui.rs": 3621, + "crates/jcode-tui/src/tui/ui.rs": 3683, "crates/jcode-tui/src/tui/ui_frame_metrics.rs": 1437, "crates/jcode-tui/src/tui/ui_header.rs": 1758, "crates/jcode-tui/src/tui/ui_inline_image.rs": 1726, "crates/jcode-tui/src/tui/ui_inline_interactive.rs": 1268, - "crates/jcode-tui/src/tui/ui_input.rs": 3108, - "crates/jcode-tui/src/tui/ui_messages.rs": 4213, + "crates/jcode-tui/src/tui/ui_input.rs": 3188, + "crates/jcode-tui/src/tui/ui_messages.rs": 4466, "crates/jcode-tui/src/tui/ui_pinned.rs": 2046, - "crates/jcode-tui/src/tui/ui_prepare.rs": 2645, + "crates/jcode-tui/src/tui/ui_prepare.rs": 2655, "crates/jcode-tui/src/tui/ui_tools.rs": 1656, - "crates/jcode-tui/src/tui/ui_viewport.rs": 1523, + "crates/jcode-tui/src/tui/ui_viewport.rs": 1524, "src/bin/memory_recall_bench.rs": 2667, "src/bin/tui_bench.rs": 1763, - "src/cli/commands.rs": 3378, - "src/cli/dispatch.rs": 1404, + "src/cli/acp.rs": 1641, + "src/cli/commands.rs": 3375, + "src/cli/dispatch.rs": 1435, "src/cli/login.rs": 1389, - "src/cli/provider_init.rs": 1836 + "src/cli/provider_init.rs": 1850 }, "version": 1 } diff --git a/scripts/panic_budget.json b/scripts/panic_budget.json index 6a7fa966c9..8486fbc92b 100644 --- a/scripts/panic_budget.json +++ b/scripts/panic_budget.json @@ -1,5 +1,5 @@ { - "total": 63, + "total": 77, "tracked_files": { "crates/jcode-app-core/src/session_launch.rs": 1, "crates/jcode-app-core/src/tool/communicate.rs": 1, @@ -7,15 +7,21 @@ "crates/jcode-base/src/auth/oauth.rs": 3, "crates/jcode-base/src/hooks.rs": 1, "crates/jcode-desktop2/src/cli.rs": 1, + "crates/jcode-desktop2/src/icons.rs": 2, "crates/jcode-desktop2/src/main.rs": 2, "crates/jcode-desktop2/src/scroll_bench.rs": 1, - "crates/jcode-desktop2/src/states.rs": 1, + "crates/jcode-desktop2/src/states.rs": 2, "crates/jcode-desktop2/src/transcript.rs": 1, + "crates/jcode-harness-api-server/src/translate.rs": 2, "crates/jcode-harness-api/examples/harness_repl.rs": 15, + "crates/jcode-math/src/font.rs": 1, + "crates/jcode-math/src/parse.rs": 4, "crates/jcode-plan/src/dag/ops.rs": 1, + "crates/jcode-provider-anthropic/src/wedge_fixture_check.rs": 3, "crates/jcode-provider-doctor/src/lifecycle_driver.rs": 2, "crates/jcode-render-core/src/math.rs": 4, "crates/jcode-render-core/src/preprocess.rs": 2, + "crates/jcode-sdk/src/structured.rs": 1, "crates/jcode-telemetry-core/src/lib.rs": 2, "crates/jcode-terminal-launch/src/lib.rs": 1, "crates/jcode-tui-core/src/stream_buffer.rs": 3, diff --git a/scripts/swallowed_error_budget.json b/scripts/swallowed_error_budget.json index 3230445724..356939ed96 100644 --- a/scripts/swallowed_error_budget.json +++ b/scripts/swallowed_error_budget.json @@ -1,9 +1,9 @@ { - "total": 3091, + "total": 3244, "totals_by_pattern": { - "dot_ok": 1157, - "let_underscore": 1168, - "unwrap_or_default": 766 + "dot_ok": 1213, + "let_underscore": 1202, + "unwrap_or_default": 829 }, "tracked_files": { "crates/jcode-app-core/src/agent.rs": { @@ -97,9 +97,9 @@ "unwrap_or_default": 0 }, "crates/jcode-app-core/src/notifications.rs": { - "dot_ok": 0, - "let_underscore": 5, - "unwrap_or_default": 1 + "dot_ok": 3, + "let_underscore": 9, + "unwrap_or_default": 4 }, "crates/jcode-app-core/src/overnight.rs": { "dot_ok": 12, @@ -153,7 +153,7 @@ }, "crates/jcode-app-core/src/server/client_lifecycle.rs": { "dot_ok": 0, - "let_underscore": 30, + "let_underscore": 33, "unwrap_or_default": 0 }, "crates/jcode-app-core/src/server/client_lifecycle_logging.rs": { @@ -436,6 +436,11 @@ "let_underscore": 1, "unwrap_or_default": 0 }, + "crates/jcode-app-core/src/tool/config_edit_notice.rs": { + "dot_ok": 0, + "let_underscore": 0, + "unwrap_or_default": 1 + }, "crates/jcode-app-core/src/tool/conversation_search.rs": { "dot_ok": 1, "let_underscore": 0, @@ -446,6 +451,11 @@ "let_underscore": 0, "unwrap_or_default": 5 }, + "crates/jcode-app-core/src/tool/discover_secrets.rs": { + "dot_ok": 0, + "let_underscore": 0, + "unwrap_or_default": 1 + }, "crates/jcode-app-core/src/tool/gmail.rs": { "dot_ok": 0, "let_underscore": 0, @@ -456,6 +466,11 @@ "let_underscore": 0, "unwrap_or_default": 4 }, + "crates/jcode-app-core/src/tool/inflight.rs": { + "dot_ok": 1, + "let_underscore": 0, + "unwrap_or_default": 0 + }, "crates/jcode-app-core/src/tool/ls.rs": { "dot_ok": 1, "let_underscore": 0, @@ -487,9 +502,9 @@ "unwrap_or_default": 1 }, "crates/jcode-app-core/src/tool/selfdev/build_queue.rs": { - "dot_ok": 1, - "let_underscore": 3, - "unwrap_or_default": 0 + "dot_ok": 2, + "let_underscore": 4, + "unwrap_or_default": 2 }, "crates/jcode-app-core/src/tool/selfdev/mod.rs": { "dot_ok": 8, @@ -524,7 +539,7 @@ "crates/jcode-app-core/src/tool/todo.rs": { "dot_ok": 0, "let_underscore": 0, - "unwrap_or_default": 8 + "unwrap_or_default": 11 }, "crates/jcode-app-core/src/tool/webfetch.rs": { "dot_ok": 3, @@ -542,7 +557,7 @@ "unwrap_or_default": 0 }, "crates/jcode-app-core/src/turn_cancel_registry.rs": { - "dot_ok": 1, + "dot_ok": 2, "let_underscore": 0, "unwrap_or_default": 1 }, @@ -573,7 +588,7 @@ }, "crates/jcode-base/src/auth/antigravity.rs": { "dot_ok": 8, - "let_underscore": 3, + "let_underscore": 1, "unwrap_or_default": 0 }, "crates/jcode-base/src/auth/claude.rs": { @@ -593,7 +608,12 @@ }, "crates/jcode-base/src/auth/cursor.rs": { "dot_ok": 10, - "let_underscore": 5, + "let_underscore": 3, + "unwrap_or_default": 0 + }, + "crates/jcode-base/src/auth/env_facts.rs": { + "dot_ok": 0, + "let_underscore": 1, "unwrap_or_default": 0 }, "crates/jcode-base/src/auth/external.rs": { @@ -603,12 +623,12 @@ }, "crates/jcode-base/src/auth/gemini.rs": { "dot_ok": 4, - "let_underscore": 3, + "let_underscore": 1, "unwrap_or_default": 0 }, "crates/jcode-base/src/auth/google.rs": { "dot_ok": 4, - "let_underscore": 2, + "let_underscore": 0, "unwrap_or_default": 0 }, "crates/jcode-base/src/auth/lifecycle.rs": { @@ -628,12 +648,12 @@ }, "crates/jcode-base/src/auth/oauth.rs": { "dot_ok": 4, - "let_underscore": 18, + "let_underscore": 13, "unwrap_or_default": 3 }, "crates/jcode-base/src/auth/refresh_state.rs": { "dot_ok": 0, - "let_underscore": 0, + "let_underscore": 3, "unwrap_or_default": 1 }, "crates/jcode-base/src/auth/test_sandbox.rs": { @@ -686,6 +706,11 @@ "let_underscore": 4, "unwrap_or_default": 2 }, + "crates/jcode-base/src/config/env_overrides.rs": { + "dot_ok": 1, + "let_underscore": 0, + "unwrap_or_default": 0 + }, "crates/jcode-base/src/copilot_usage.rs": { "dot_ok": 0, "let_underscore": 1, @@ -738,7 +763,7 @@ }, "crates/jcode-base/src/hooks.rs": { "dot_ok": 0, - "let_underscore": 1, + "let_underscore": 2, "unwrap_or_default": 0 }, "crates/jcode-base/src/import.rs": { @@ -756,8 +781,18 @@ "let_underscore": 4, "unwrap_or_default": 0 }, + "crates/jcode-base/src/mcp/manager.rs": { + "dot_ok": 1, + "let_underscore": 0, + "unwrap_or_default": 0 + }, + "crates/jcode-base/src/mcp/pool.rs": { + "dot_ok": 2, + "let_underscore": 0, + "unwrap_or_default": 0 + }, "crates/jcode-base/src/mcp/protocol.rs": { - "dot_ok": 0, + "dot_ok": 2, "let_underscore": 0, "unwrap_or_default": 2 }, @@ -832,7 +867,7 @@ "unwrap_or_default": 0 }, "crates/jcode-base/src/prompt.rs": { - "dot_ok": 10, + "dot_ok": 11, "let_underscore": 0, "unwrap_or_default": 0 }, @@ -954,7 +989,7 @@ "crates/jcode-base/src/session/persistence.rs": { "dot_ok": 0, "let_underscore": 1, - "unwrap_or_default": 0 + "unwrap_or_default": 1 }, "crates/jcode-base/src/session/storage_paths.rs": { "dot_ok": 0, @@ -1006,20 +1041,15 @@ "let_underscore": 0, "unwrap_or_default": 3 }, - "crates/jcode-base/src/todo.rs": { - "dot_ok": 1, - "let_underscore": 0, - "unwrap_or_default": 4 - }, - "crates/jcode-base/src/transport/unix.rs": { + "crates/jcode-base/src/terminal_launch.rs": { "dot_ok": 0, "let_underscore": 1, "unwrap_or_default": 0 }, - "crates/jcode-base/src/transport/windows.rs": { - "dot_ok": 0, - "let_underscore": 1, - "unwrap_or_default": 0 + "crates/jcode-base/src/todo.rs": { + "dot_ok": 1, + "let_underscore": 0, + "unwrap_or_default": 10 }, "crates/jcode-base/src/usage.rs": { "dot_ok": 0, @@ -1126,6 +1156,16 @@ "let_underscore": 1, "unwrap_or_default": 2 }, + "crates/jcode-desktop2/src/app_resume.rs": { + "dot_ok": 0, + "let_underscore": 2, + "unwrap_or_default": 1 + }, + "crates/jcode-desktop2/src/app_workspace.rs": { + "dot_ok": 0, + "let_underscore": 0, + "unwrap_or_default": 1 + }, "crates/jcode-desktop2/src/capture.rs": { "dot_ok": 0, "let_underscore": 1, @@ -1141,25 +1181,30 @@ "let_underscore": 0, "unwrap_or_default": 0 }, + "crates/jcode-desktop2/src/clipboard_image.rs": { + "dot_ok": 2, + "let_underscore": 0, + "unwrap_or_default": 0 + }, "crates/jcode-desktop2/src/editor.rs": { "dot_ok": 0, "let_underscore": 0, "unwrap_or_default": 1 }, "crates/jcode-desktop2/src/edits.rs": { - "dot_ok": 0, + "dot_ok": 1, "let_underscore": 0, - "unwrap_or_default": 1 + "unwrap_or_default": 2 }, "crates/jcode-desktop2/src/harness.rs": { "dot_ok": 1, "let_underscore": 1, - "unwrap_or_default": 2 + "unwrap_or_default": 4 }, "crates/jcode-desktop2/src/main.rs": { "dot_ok": 0, "let_underscore": 2, - "unwrap_or_default": 0 + "unwrap_or_default": 1 }, "crates/jcode-desktop2/src/mem.rs": { "dot_ok": 4, @@ -1181,21 +1226,56 @@ "let_underscore": 0, "unwrap_or_default": 0 }, + "crates/jcode-desktop2/src/png.rs": { + "dot_ok": 2, + "let_underscore": 0, + "unwrap_or_default": 0 + }, "crates/jcode-desktop2/src/reasoning.rs": { "dot_ok": 3, "let_underscore": 0, "unwrap_or_default": 1 }, + "crates/jcode-desktop2/src/resume.rs": { + "dot_ok": 4, + "let_underscore": 0, + "unwrap_or_default": 2 + }, "crates/jcode-desktop2/src/scene.rs": { "dot_ok": 0, "let_underscore": 0, "unwrap_or_default": 1 }, + "crates/jcode-desktop2/src/scene_workspace.rs": { + "dot_ok": 0, + "let_underscore": 0, + "unwrap_or_default": 1 + }, "crates/jcode-desktop2/src/scroll_bench.rs": { "dot_ok": 0, "let_underscore": 0, "unwrap_or_default": 1 }, + "crates/jcode-desktop2/src/selfdev_reload.rs": { + "dot_ok": 2, + "let_underscore": 1, + "unwrap_or_default": 0 + }, + "crates/jcode-desktop2/src/syntax.rs": { + "dot_ok": 0, + "let_underscore": 0, + "unwrap_or_default": 1 + }, + "crates/jcode-desktop2/src/todos.rs": { + "dot_ok": 1, + "let_underscore": 0, + "unwrap_or_default": 0 + }, + "crates/jcode-desktop2/src/transcript.rs": { + "dot_ok": 0, + "let_underscore": 0, + "unwrap_or_default": 1 + }, "crates/jcode-embedding/src/lib.rs": { "dot_ok": 2, "let_underscore": 0, @@ -1212,14 +1292,14 @@ "unwrap_or_default": 0 }, "crates/jcode-harness-api-server/src/lib.rs": { - "dot_ok": 1, - "let_underscore": 1, + "dot_ok": 2, + "let_underscore": 2, "unwrap_or_default": 0 }, "crates/jcode-harness-api-server/src/translate.rs": { - "dot_ok": 4, - "let_underscore": 0, - "unwrap_or_default": 5 + "dot_ok": 12, + "let_underscore": 2, + "unwrap_or_default": 23 }, "crates/jcode-harness-api/examples/harness_repl.rs": { "dot_ok": 0, @@ -1251,6 +1331,16 @@ "let_underscore": 1, "unwrap_or_default": 1 }, + "crates/jcode-math/src/font.rs": { + "dot_ok": 1, + "let_underscore": 0, + "unwrap_or_default": 0 + }, + "crates/jcode-math/src/parse.rs": { + "dot_ok": 0, + "let_underscore": 5, + "unwrap_or_default": 3 + }, "crates/jcode-memory-types/src/graph.rs": { "dot_ok": 0, "let_underscore": 0, @@ -1308,17 +1398,12 @@ }, "crates/jcode-provider-anthropic-runtime/src/lib.rs": { "dot_ok": 4, - "let_underscore": 14, - "unwrap_or_default": 1 - }, - "crates/jcode-provider-anthropic/src/lib.rs": { - "dot_ok": 0, - "let_underscore": 0, + "let_underscore": 15, "unwrap_or_default": 1 }, "crates/jcode-provider-antigravity-runtime/src/lib.rs": { "dot_ok": 2, - "let_underscore": 18, + "let_underscore": 19, "unwrap_or_default": 3 }, "crates/jcode-provider-antigravity/src/lib.rs": { @@ -1418,7 +1503,7 @@ }, "crates/jcode-provider-gemini-runtime/src/lib.rs": { "dot_ok": 4, - "let_underscore": 23, + "let_underscore": 24, "unwrap_or_default": 6 }, "crates/jcode-provider-gemini/src/lib.rs": { @@ -1467,10 +1552,15 @@ "unwrap_or_default": 6 }, "crates/jcode-provider-openrouter-runtime/src/lib.rs": { - "dot_ok": 23, + "dot_ok": 22, "let_underscore": 0, "unwrap_or_default": 9 }, + "crates/jcode-provider-openrouter-runtime/src/models_catalog_parse.rs": { + "dot_ok": 1, + "let_underscore": 0, + "unwrap_or_default": 0 + }, "crates/jcode-provider-openrouter-runtime/src/openrouter_provider_impl.rs": { "dot_ok": 2, "let_underscore": 3, @@ -1489,7 +1579,7 @@ "crates/jcode-provider-openrouter/src/request.rs": { "dot_ok": 0, "let_underscore": 0, - "unwrap_or_default": 2 + "unwrap_or_default": 1 }, "crates/jcode-render-core/src/math.rs": { "dot_ok": 0, @@ -1501,6 +1591,36 @@ "let_underscore": 0, "unwrap_or_default": 1 }, + "crates/jcode-schema-dialect/src/dialect.rs": { + "dot_ok": 0, + "let_underscore": 0, + "unwrap_or_default": 3 + }, + "crates/jcode-schema-dialect/src/lib.rs": { + "dot_ok": 0, + "let_underscore": 0, + "unwrap_or_default": 1 + }, + "crates/jcode-schema-dialect/src/quirks.rs": { + "dot_ok": 2, + "let_underscore": 2, + "unwrap_or_default": 2 + }, + "crates/jcode-sdk/src/client.rs": { + "dot_ok": 6, + "let_underscore": 4, + "unwrap_or_default": 0 + }, + "crates/jcode-sdk/src/launch.rs": { + "dot_ok": 5, + "let_underscore": 7, + "unwrap_or_default": 3 + }, + "crates/jcode-sdk/src/structured.rs": { + "dot_ok": 0, + "let_underscore": 0, + "unwrap_or_default": 2 + }, "crates/jcode-session-types/src/lib.rs": { "dot_ok": 0, "let_underscore": 0, @@ -1567,13 +1687,13 @@ "unwrap_or_default": 1 }, "crates/jcode-telemetry-core/src/lib.rs": { - "dot_ok": 7, + "dot_ok": 8, "let_underscore": 10, "unwrap_or_default": 3 }, "crates/jcode-telemetry-core/src/lifecycle.rs": { "dot_ok": 0, - "let_underscore": 2, + "let_underscore": 3, "unwrap_or_default": 0 }, "crates/jcode-telemetry-core/src/state_support.rs": { @@ -1591,6 +1711,16 @@ "let_underscore": 0, "unwrap_or_default": 0 }, + "crates/jcode-transport/src/unix.rs": { + "dot_ok": 0, + "let_underscore": 1, + "unwrap_or_default": 0 + }, + "crates/jcode-transport/src/windows.rs": { + "dot_ok": 0, + "let_underscore": 1, + "unwrap_or_default": 0 + }, "crates/jcode-tui-core/src/keybind.rs": { "dot_ok": 1, "let_underscore": 0, @@ -1608,7 +1738,7 @@ }, "crates/jcode-tui-markdown/src/markdown_latex_image.rs": { "dot_ok": 4, - "let_underscore": 5, + "let_underscore": 6, "unwrap_or_default": 0 }, "crates/jcode-tui-markdown/src/markdown_mermaid_fallback.rs": { @@ -1734,7 +1864,7 @@ "crates/jcode-tui/src/tui/app/auth.rs": { "dot_ok": 7, "let_underscore": 3, - "unwrap_or_default": 12 + "unwrap_or_default": 13 }, "crates/jcode-tui/src/tui/app/auth_account_commands.rs": { "dot_ok": 0, @@ -1852,9 +1982,9 @@ "unwrap_or_default": 1 }, "crates/jcode-tui/src/tui/app/input.rs": { - "dot_ok": 8, + "dot_ok": 9, "let_underscore": 3, - "unwrap_or_default": 3 + "unwrap_or_default": 4 }, "crates/jcode-tui/src/tui/app/local.rs": { "dot_ok": 0, @@ -2019,7 +2149,7 @@ "crates/jcode-tui/src/tui/app/turn_notify.rs": { "dot_ok": 0, "let_underscore": 0, - "unwrap_or_default": 2 + "unwrap_or_default": 4 }, "crates/jcode-tui/src/tui/app/ui_prefs.rs": { "dot_ok": 1, @@ -2234,7 +2364,7 @@ "src/cli/acp.rs": { "dot_ok": 1, "let_underscore": 5, - "unwrap_or_default": 2 + "unwrap_or_default": 3 }, "src/cli/commands.rs": { "dot_ok": 11, @@ -2281,6 +2411,11 @@ "let_underscore": 2, "unwrap_or_default": 0 }, + "src/cli/macos_notification_broker.rs": { + "dot_ok": 6, + "let_underscore": 4, + "unwrap_or_default": 0 + }, "src/cli/provider_init.rs": { "dot_ok": 2, "let_underscore": 14, diff --git a/scripts/test_size_budget.json b/scripts/test_size_budget.json index b81a891543..7e0d69e111 100644 --- a/scripts/test_size_budget.json +++ b/scripts/test_size_budget.json @@ -1,25 +1,28 @@ { "threshold_loc": 1200, "tracked_files": { - "crates/jcode-app-core/src/agent_tests.rs": 1585, - "crates/jcode-app-core/src/server/client_lifecycle_tests.rs": 1232, + "crates/jcode-app-core/src/agent_tests.rs": 1760, + "crates/jcode-app-core/src/server/client_lifecycle_tests.rs": 1410, "crates/jcode-app-core/src/server/comm_control_tests/dag_e2e.rs": 1332, "crates/jcode-app-core/src/server/provider_control_tests.rs": 1393, "crates/jcode-app-core/src/server/swarm_persistence_tests.rs": 1232, - "crates/jcode-app-core/src/tool/communicate_tests.rs": 1795, + "crates/jcode-app-core/src/tool/communicate_tests.rs": 1796, "crates/jcode-app-core/src/tool/selfdev/tests.rs": 1440, - "crates/jcode-base/src/config_tests.rs": 1284, + "crates/jcode-app-core/src/tool/tests.rs": 1586, + "crates/jcode-base/src/config_tests.rs": 1335, "crates/jcode-base/src/live_tests.rs": 3087, - "crates/jcode-base/src/provider/tests/model_resolution.rs": 2337, - "crates/jcode-base/src/session_tests/cases.rs": 2448, - "crates/jcode-desktop2/src/tests/actions.rs": 1296, + "crates/jcode-base/src/provider/tests/model_resolution.rs": 2350, + "crates/jcode-base/src/session_tests/cases.rs": 2465, + "crates/jcode-desktop2/src/tests/actions.rs": 1518, + "crates/jcode-desktop2/src/tests/visual.rs": 1245, + "crates/jcode-harness-api-server/src/translate_tests.rs": 1390, "crates/jcode-plan/src/dag/tests.rs": 1392, - "crates/jcode-provider-anthropic-runtime/src/anthropic_tests.rs": 1937, - "crates/jcode-provider-openrouter-runtime/src/openrouter_tests.rs": 3014, - "crates/jcode-tui/src/tui/app/tests.rs": 1749, - "crates/jcode-tui/src/tui/app/tests/commands_accounts_01/part_01.rs": 1633, + "crates/jcode-provider-anthropic-runtime/src/anthropic_tests.rs": 2017, + "crates/jcode-provider-openrouter-runtime/src/openrouter_tests.rs": 3119, + "crates/jcode-tui/src/tui/app/tests.rs": 1750, + "crates/jcode-tui/src/tui/app/tests/commands_accounts_01/part_01.rs": 1673, "crates/jcode-tui/src/tui/app/tests/onboarding_eval.rs": 3302, - "crates/jcode-tui/src/tui/app/tests/onboarding_flow.rs": 1755, + "crates/jcode-tui/src/tui/app/tests/onboarding_flow.rs": 1784, "crates/jcode-tui/src/tui/app/tests/remote_events_reload_01/part_01.rs": 1930, "crates/jcode-tui/src/tui/app/tests/remote_events_reload_04.rs": 2415, "crates/jcode-tui/src/tui/app/tests/remote_startup_input_02/part_01.rs": 1998, @@ -27,17 +30,18 @@ "crates/jcode-tui/src/tui/app/tests/scroll_copy_02/part_01.rs": 1442, "crates/jcode-tui/src/tui/app/tests/scroll_copy_02/part_02.rs": 1272, "crates/jcode-tui/src/tui/app/tests/scroll_copy_03.rs": 1842, - "crates/jcode-tui/src/tui/app/tests/state_model_poke_01/part_01.rs": 1294, - "crates/jcode-tui/src/tui/app/tests/state_model_poke_02/part_01.rs": 1361, - "crates/jcode-tui/src/tui/app/tests/state_model_poke_03.rs": 2767, - "crates/jcode-tui/src/tui/app/tests/swarm_plan_graph_inline.rs": 1971, - "crates/jcode-tui/src/tui/info_widget_tests.rs": 1810, + "crates/jcode-tui/src/tui/app/tests/state_model_poke_01/part_01.rs": 1356, + "crates/jcode-tui/src/tui/app/tests/state_model_poke_02/part_01.rs": 1376, + "crates/jcode-tui/src/tui/app/tests/state_model_poke_03.rs": 2867, + "crates/jcode-tui/src/tui/app/tests/swarm_plan_graph_inline.rs": 1979, + "crates/jcode-tui/src/tui/info_widget_tests.rs": 1815, "crates/jcode-tui/src/tui/session_picker/loading_tests.rs": 1401, "crates/jcode-tui/src/tui/session_picker_tests.rs": 2453, - "crates/jcode-tui/src/tui/ui_messages/tests.rs": 2908, + "crates/jcode-tui/src/tui/ui_messages/tests.rs": 3154, "crates/jcode-tui/src/tui/ui_tests/prepare.rs": 1285, "crates/jcode-tui/src/tui/ui_tests/tools.rs": 1352, - "tests/e2e/test_support/mod.rs": 1424 + "src/cli/commands_tests.rs": 1274, + "tests/e2e/test_support/mod.rs": 1425 }, "version": 1 } diff --git a/scripts/wildcard_reexport_budget.json b/scripts/wildcard_reexport_budget.json index 77ee906070..eb52b3daf1 100644 --- a/scripts/wildcard_reexport_budget.json +++ b/scripts/wildcard_reexport_budget.json @@ -1,5 +1,5 @@ { - "total": 16, + "total": 17, "files": { "crates/jcode-app-core/src/lib.rs": 1, "crates/jcode-app-core/src/setup_hints.rs": 1, @@ -13,6 +13,7 @@ "crates/jcode-base/src/provider_catalog.rs": 1, "crates/jcode-base/src/stdin_detect.rs": 1, "crates/jcode-base/src/storage.rs": 1, + "crates/jcode-base/src/transport/mod.rs": 1, "crates/jcode-base/src/util.rs": 1, "crates/jcode-tui/src/lib.rs": 1, "crates/jcode-tui/src/tui/mod.rs": 1, From e675cbd8891b0adeb745effbec0163498bcef78d Mon Sep 17 00:00:00 2001 From: jeremy <94247773+1jehuang@users.noreply.github.com> Date: Thu, 6 Aug 2026 02:29:27 -0700 Subject: [PATCH 26/45] test(todo): assert hidden passing card gates --- crates/jcode-tui/src/tui/ui_messages/tests.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/crates/jcode-tui/src/tui/ui_messages/tests.rs b/crates/jcode-tui/src/tui/ui_messages/tests.rs index 5afaa3c34f..de22b65e39 100644 --- a/crates/jcode-tui/src/tui/ui_messages/tests.rs +++ b/crates/jcode-tui/src/tui/ui_messages/tests.rs @@ -1223,7 +1223,11 @@ fn unbiased_visual_prompt_retry_renders_complete_feedback_change() { ); let compact_revised = without_whitespace(&revised); assert!(revised.contains("pelican-bike-animation"), "{revised}"); - assert!(revised.contains("Closed feedback loop closed"), "{revised}"); + assert!( + revised.contains("Relevance missing · Coverage missing · Traceability missing"), + "{revised}" + ); + assert!(!revised.contains("Closed feedback loop closed")); assert!(!compact_revised.contains(&without_whitespace(REVISED_FEEDBACK))); assert!(revised.contains("● Implement"), "{revised}"); } From 145b8ba9695b59f8e9cd85431cf9d490fe0f904c Mon Sep 17 00:00:00 2001 From: jeremy <94247773+1jehuang@users.noreply.github.com> Date: Thu, 6 Aug 2026 02:42:20 -0700 Subject: [PATCH 27/45] Offer Jcode subscription during onboarding --- .../jcode-tui/src/tui/app/onboarding_flow.rs | 7 ++++-- .../src/tui/app/onboarding_flow_control.rs | 13 +++++++++++ .../src/tui/app/state_ui_input_helpers.rs | 3 +++ .../src/tui/app/tests/onboarding_golden.rs | 22 +++++++++++-------- crates/jcode-tui/src/tui/mod.rs | 4 +++- crates/jcode-tui/src/tui/ui_onboarding.rs | 20 ++++++++++++----- 6 files changed, 51 insertions(+), 18 deletions(-) diff --git a/crates/jcode-tui/src/tui/app/onboarding_flow.rs b/crates/jcode-tui/src/tui/app/onboarding_flow.rs index ceb3dcb6b9..43d3947505 100644 --- a/crates/jcode-tui/src/tui/app/onboarding_flow.rs +++ b/crates/jcode-tui/src/tui/app/onboarding_flow.rs @@ -149,11 +149,13 @@ pub(crate) struct ImportReview { pub(crate) shown_at: Instant, } -/// The three actions on the import summary screen, left to right. +/// The actions on the import summary screen, left to right. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub(crate) enum SummaryPill { /// Import every detected login and move on (default). Continue, + /// Skip importing and sign in with a Jcode subscription instead. + Subscription, /// Open the per-login checkbox list to import fewer logins. ImportLess, /// Open the telemetry settings sub-page. @@ -161,8 +163,9 @@ pub(crate) enum SummaryPill { } impl SummaryPill { - const ORDER: [SummaryPill; 3] = [ + const ORDER: [SummaryPill; 4] = [ SummaryPill::Continue, + SummaryPill::Subscription, SummaryPill::ImportLess, SummaryPill::Telemetry, ]; diff --git a/crates/jcode-tui/src/tui/app/onboarding_flow_control.rs b/crates/jcode-tui/src/tui/app/onboarding_flow_control.rs index e6da7c5f8c..9942f31367 100644 --- a/crates/jcode-tui/src/tui/app/onboarding_flow_control.rs +++ b/crates/jcode-tui/src/tui/app/onboarding_flow_control.rs @@ -550,6 +550,9 @@ impl App { // `finished` means the user committed the import (so we kick it off // outside the borrow). let mut finished = false; + // Set when the user chooses the hosted Jcode subscription instead of + // importing one of the detected third-party logins. + let mut start_subscription = false; // Set when the user committed a telemetry level, so we persist it // outside the review borrow. let mut telemetry_choice = None; @@ -599,6 +602,7 @@ impl App { KeyCode::Char('y') | KeyCode::Char('Y') => finished = true, KeyCode::Enter | KeyCode::Char(' ') => match review.summary_pill { SummaryPill::Continue => finished = true, + SummaryPill::Subscription => start_subscription = true, SummaryPill::ImportLess => review.enter_choose_mode(), SummaryPill::Telemetry => review.open_telemetry(), }, @@ -631,6 +635,15 @@ impl App { self.set_status_notice(level.status_label().to_string()); return true; } + if start_subscription { + self.onboarding_import_error = None; + if let Some(provider) = crate::provider_catalog::resolve_login_provider("jcode") { + self.start_login_provider(provider); + } else { + self.set_status_notice("Jcode subscription login is unavailable".to_string()); + } + return true; + } if finished { self.onboarding_finish_import_review(); } else { diff --git a/crates/jcode-tui/src/tui/app/state_ui_input_helpers.rs b/crates/jcode-tui/src/tui/app/state_ui_input_helpers.rs index 857b889eda..8d2d566a70 100644 --- a/crates/jcode-tui/src/tui/app/state_ui_input_helpers.rs +++ b/crates/jcode-tui/src/tui/app/state_ui_input_helpers.rs @@ -1382,6 +1382,9 @@ impl App { choosing: review.choosing, summary_pill: match review.summary_pill { SummaryPill::Continue => crate::tui::ImportSummaryPill::Continue, + SummaryPill::Subscription => { + crate::tui::ImportSummaryPill::Subscription + } SummaryPill::ImportLess => crate::tui::ImportSummaryPill::ImportLess, SummaryPill::Telemetry => crate::tui::ImportSummaryPill::Telemetry, }, diff --git a/crates/jcode-tui/src/tui/app/tests/onboarding_golden.rs b/crates/jcode-tui/src/tui/app/tests/onboarding_golden.rs index f8802a3fe1..a6a6f522cf 100644 --- a/crates/jcode-tui/src/tui/app/tests/onboarding_golden.rs +++ b/crates/jcode-tui/src/tui/app/tests/onboarding_golden.rs @@ -108,7 +108,8 @@ fn onboarding_golden_walks_every_phase() { // 2. Login with detected imports: the default SUMMARY screen. It lists // everything we detected read-only and lands focus on a preselected - // "Continue" pill, with "Import less" and "Telemetry settings" beside it. + // import action, with a Jcode subscription alternative and secondary + // import/telemetry controls beside it. { let review = ImportReview::new(vec![ ExternalAuthReviewCandidate::fixture("OpenAI/Codex", "Codex auth.json"), @@ -130,15 +131,14 @@ fn onboarding_golden_walks_every_phase() { assert!(text.contains("Codex auth.json"), "source 1: {text}"); assert!(text.contains("Claude"), "provider 2: {text}"); assert!(text.contains('✓'), "detected checkmark: {text}"); - // The three action pills: "Continue" (preselected), "Import less", - // and "Telemetry settings", drawn as lozenges with half-circle end - // caps (◖ ◗). - assert!(text.contains("Continue"), "continue pill label: {text}"); - assert!(text.contains("Import less"), "import-less pill: {text}"); + // The primary actions explicitly offer import or a Jcode subscription. + assert!(text.contains("Import"), "import pill label: {text}"); assert!( - text.contains("Telemetry settings"), - "telemetry pill label: {text}" + text.contains("Use Jcode subscription"), + "subscription pill label: {text}" ); + assert!(text.contains("Import less"), "import-less pill: {text}"); + assert!(text.contains("Telemetry"), "telemetry pill label: {text}"); assert!( text.contains('\u{25D6}') && text.contains('\u{25D7}'), "pill rounded end caps: {text}" @@ -199,7 +199,11 @@ fn onboarding_golden_walks_every_phase() { "singular headline: {text}" ); assert!(text.contains("Cursor"), "single login row: {text}"); - assert!(text.contains("Continue"), "continue pill: {text}"); + assert!(text.contains("Import"), "import pill: {text}"); + assert!( + text.contains("Use Jcode subscription"), + "subscription pill: {text}" + ); } // 4. Continue prompt (resume an external session). diff --git a/crates/jcode-tui/src/tui/mod.rs b/crates/jcode-tui/src/tui/mod.rs index 9229727072..9e6b2243ea 100644 --- a/crates/jcode-tui/src/tui/mod.rs +++ b/crates/jcode-tui/src/tui/mod.rs @@ -970,11 +970,13 @@ pub struct LoginImportPrompt { pub seconds_left: u64, } -/// The three actions on the import summary screen, left to right. +/// The actions on the import summary screen, left to right. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ImportSummaryPill { /// Import everything we detected (default). Continue, + /// Sign in with a Jcode subscription instead of importing. + Subscription, /// Open the per-login checkbox list to import fewer logins. ImportLess, /// Open the telemetry settings sub-page. diff --git a/crates/jcode-tui/src/tui/ui_onboarding.rs b/crates/jcode-tui/src/tui/ui_onboarding.rs index dde39085d5..6f3d5b4523 100644 --- a/crates/jcode-tui/src/tui/ui_onboarding.rs +++ b/crates/jcode-tui/src/tui/ui_onboarding.rs @@ -135,16 +135,24 @@ fn continue_pill_line(focused: bool, align: Alignment) -> Line<'static> { Line::from(lozenge_pill_spans("Continue", focused)).alignment(align) } -/// The summary-screen action row: "Continue" (imports everything, preselected) -/// next to "Import less" (opens the per-login checkbox list) and "Telemetry -/// settings" (opens the telemetry sub-page). +/// The summary-screen action row. A new user can import the logins we found or +/// use a Jcode subscription, with secondary controls for a selective import and +/// telemetry settings. fn import_summary_pills_line( focused: crate::tui::ImportSummaryPill, align: Alignment, ) -> Line<'static> { use crate::tui::ImportSummaryPill as Pill; let mut spans = Vec::new(); - spans.extend(lozenge_pill_spans("Continue", focused == Pill::Continue)); + spans.extend(lozenge_pill_spans( + "Import", + focused == Pill::Continue, + )); + spans.push(Span::raw(" ")); + spans.extend(lozenge_pill_spans( + "Use Jcode subscription", + focused == Pill::Subscription, + )); spans.push(Span::raw(" ")); spans.extend(lozenge_pill_spans( "Import less", @@ -152,7 +160,7 @@ fn import_summary_pills_line( )); spans.push(Span::raw(" ")); spans.extend(lozenge_pill_spans( - "Telemetry settings", + "Telemetry", focused == Pill::Telemetry, )); Line::from(spans).alignment(align) @@ -521,7 +529,7 @@ fn welcome_body_lines(app: &dyn TuiState) -> Vec> { lines.push( Line::from(Span::styled( format!( - "We found {found} existing login{}:", + "Choose how to get started. We found {found} existing login{}:", if found == 1 { "" } else { "s" } ), Style::default() From fd3941e9eefac09810fbf462877ebf4c9d3f1528 Mon Sep 17 00:00:00 2001 From: jeremy <94247773+1jehuang@users.noreply.github.com> Date: Thu, 6 Aug 2026 02:43:56 -0700 Subject: [PATCH 28/45] fix(discovery): validate selection receipts and benchmark identity --- crates/jcode-app-core/src/tool/discover.rs | 186 ++++++++++++++++----- docs/DISCOVERY_RATE_BENCHMARK.md | 6 +- scripts/benchmark_discovery_rate.py | 50 ++++++ scripts/test_benchmark_discovery_rate.py | 24 +++ 4 files changed, 224 insertions(+), 42 deletions(-) diff --git a/crates/jcode-app-core/src/tool/discover.rs b/crates/jcode-app-core/src/tool/discover.rs index 5fb2f3afb4..fed3f231ee 100644 --- a/crates/jcode-app-core/src/tool/discover.rs +++ b/crates/jcode-app-core/src/tool/discover.rs @@ -58,16 +58,16 @@ fn listing_has_no_tool_entry(listing: &Value) -> bool { } } -/// Error shown when a select names something outside the catalog. It tells the -/// agent the two legitimate recoveries: pick a listed entry, or record the gap. -fn off_catalog_select_error(category: &str, tool_name: &str) -> anyhow::Error { +/// Error shown when the server cannot return a valid receipt for a selection. +/// Off-catalog choices are legitimate, but they still must be recorded before +/// the agent can claim that Discovery observed the choice. +fn selection_receipt_error(category: &str, tool_name: &str) -> anyhow::Error { anyhow::anyhow!( - "'{tool_name}' is not in the Jcode catalog for '{category}'. Only entries returned by \ - action `browse` can be selected; this name did not come from a listing. Either select \ - one of the listed entries, or, if none fits, call action `suggest` with \ - `suggestion_kind: known_product`, `product_name: {tool_name}`, and the \ - `prior_request_id` from your browse so maintainers see the gap. Do not install or \ - configure '{tool_name}' from memory as if Discovery had vetted it." + "Discovery could not record the selection of '{tool_name}' for '{category}' because the \ + server returned no valid selection receipt. Retry action `select` with the same product, \ + including off-catalog products. Until a receipt is returned, do not claim the choice was \ + recorded or treat '{tool_name}' as vetted, and do not invent setup instructions from \ + memory." ) } @@ -690,11 +690,9 @@ impl Tool for DiscoverToolsTool { let fetched = match fetch_listing(&discovery_request, Some(&tool_name)).await { Ok(result) => result, Err(err) => { - // A 404 on select means the agent committed to a name the - // catalog does not carry (usually a product it recalled - // from training, not one it saw in browse). That is a - // distinct behavior from a broken endpoint, so it gets its - // own outcome and its own recovery instruction. + // Older endpoints returned 404 for an off-catalog choice. + // Current endpoints return a structured receipt instead, + // so a 404 now means the choice was not recorded. if err.http_status == Some(404) { record_discovery_telemetry( &request_id, @@ -711,7 +709,7 @@ impl Tool for DiscoverToolsTool { query_present, reason_present, ); - return Err(off_catalog_select_error(&category, &tool_name)); + return Err(selection_receipt_error(&category, &tool_name)); } record_discovery_telemetry( &request_id, @@ -731,8 +729,8 @@ impl Tool for DiscoverToolsTool { return Err(err.into()); } }; - // Endpoints may also answer 200 with an empty entry. Same meaning: - // the selected name is not in the catalog. + // Older endpoints may answer 200 with an empty entry. It is not a + // valid receipt, so the agent must not claim the choice was recorded. if listing_has_no_tool_entry(&fetched.listing) { record_discovery_telemetry( &request_id, @@ -749,7 +747,7 @@ impl Tool for DiscoverToolsTool { query_present, reason_present, ); - return Err(off_catalog_select_error(&category, &tool_name)); + return Err(selection_receipt_error(&category, &tool_name)); } let rendered = match render_selection(&category, &tool_name, &fetched.listing) { Ok(rendered) => rendered, @@ -1390,16 +1388,37 @@ fn render_suggestion( /// `{ "selected_tool": "...", "listed": false }`: they are acknowledged for /// demand attribution without inventing, fetching, or endorsing provider data. fn render_selection(category: &str, tool_name: &str, listing: &Value) -> Result { - let Some(tool) = listing.get("tool") else { - let selected_tool = listing - .get("selected_tool") - .and_then(Value::as_str) - .unwrap_or_default(); - if listing.get("listed").and_then(Value::as_bool) != Some(false) - || !selected_tool.eq_ignore_ascii_case(tool_name) - { + let receipt_category = listing + .get("category") + .and_then(Value::as_str) + .filter(|value| !value.trim().is_empty()) + .ok_or_else(|| anyhow::anyhow!("discovery selection receipt omitted its category"))?; + if !receipt_category.eq_ignore_ascii_case(category) { + return Err(anyhow::anyhow!( + "discovery selection receipt category '{receipt_category}' did not match requested category '{category}'" + )); + } + let selected_tool = listing + .get("selected_tool") + .and_then(Value::as_str) + .filter(|value| !value.trim().is_empty()) + .ok_or_else(|| { + anyhow::anyhow!("discovery selection receipt omitted the selected product") + })?; + if !selected_tool.eq_ignore_ascii_case(tool_name) { + return Err(anyhow::anyhow!( + "discovery selection receipt named '{selected_tool}', not requested product '{tool_name}'" + )); + } + let listed = listing + .get("listed") + .and_then(Value::as_bool) + .ok_or_else(|| anyhow::anyhow!("discovery selection receipt omitted catalog status"))?; + + if !listed { + if listing.get("tool").is_some() { return Err(anyhow::anyhow!( - "discovery returned no selection receipt for '{tool_name}'" + "off-catalog selection receipt for '{selected_tool}' unexpectedly included provider details" )); } return Ok(format!( @@ -1408,11 +1427,31 @@ fn render_selection(category: &str, tool_name: &str, listing: &Value) -> Result< product, so no provider information, recommendation, or setup instructions \ are provided. Continue using only information independently available to you." )); - }; + } + + let tool = listing + .get("tool") + .and_then(Value::as_object) + .ok_or_else(|| { + anyhow::anyhow!("catalog selection receipt contained no provider details") + })?; let name = tool .get("name") - .and_then(|v| v.as_str()) - .unwrap_or(tool_name); + .and_then(Value::as_str) + .filter(|value| !value.trim().is_empty()) + .ok_or_else(|| anyhow::anyhow!("catalog selection receipt omitted the provider name"))?; + if !name.eq_ignore_ascii_case(tool_name) || !name.eq_ignore_ascii_case(selected_tool) { + return Err(anyhow::anyhow!( + "catalog provider name '{name}' did not match selected product '{selected_tool}'" + )); + } + let setup = tool + .get("setup") + .and_then(Value::as_str) + .filter(|value| !value.trim().is_empty()) + .ok_or_else(|| { + anyhow::anyhow!("catalog selection receipt for '{name}' omitted setup instructions") + })?; let blurb = tool.get("blurb").and_then(|v| v.as_str()).unwrap_or(""); let mut out = format!( "Selected '{name}' from '{category}' (Jcode tool directory; the choice must be based only \ @@ -1422,9 +1461,7 @@ fn render_selection(category: &str, tool_name: &str, listing: &Value) -> Result< if let Some(url) = tool.get("url").and_then(|v| v.as_str()) { out.push_str(&format!(" ({url})")); } - if let Some(setup) = tool.get("setup").and_then(|v| v.as_str()) { - out.push_str(&format!("\n\nSetup: {setup}")); - } + out.push_str(&format!("\n\nSetup: {setup}")); out.push_str( "\n\nConsequential actions (signups, spending) must note the partnership in \ the confirmation shown to the user.", @@ -1570,6 +1607,9 @@ mod tests { #[test] fn render_selection_includes_setup_and_disclosure() { let listing = json!({ + "category": "payments", + "selected_tool": "agentcard", + "listed": true, "tool": { "name": "agentcard", "blurb": "virtual cards", @@ -1585,6 +1625,56 @@ mod tests { assert!(render_selection("payments", "ghost", &json!({})).is_err()); } + #[test] + fn selection_receipt_must_match_the_request_and_catalog_contract() { + let valid = json!({ + "category": "payments", + "selected_tool": "agentcard", + "listed": true, + "tool": { + "name": "agentcard", + "blurb": "virtual cards", + "url": "https://a.example", + "setup": "npm install -g agentcard" + } + }); + + let mut wrong_category = valid.clone(); + wrong_category["category"] = json!("web-data"); + assert!(render_selection("payments", "agentcard", &wrong_category).is_err()); + + let mut wrong_selected_tool = valid.clone(); + wrong_selected_tool["selected_tool"] = json!("other"); + assert!(render_selection("payments", "agentcard", &wrong_selected_tool).is_err()); + + let mut wrong_provider_name = valid.clone(); + wrong_provider_name["tool"]["name"] = json!("other"); + assert!(render_selection("payments", "agentcard", &wrong_provider_name).is_err()); + + let mut missing_status = valid.clone(); + missing_status.as_object_mut().unwrap().remove("listed"); + assert!(render_selection("payments", "agentcard", &missing_status).is_err()); + + let mut non_object_tool = valid.clone(); + non_object_tool["tool"] = json!("agentcard"); + assert!(render_selection("payments", "agentcard", &non_object_tool).is_err()); + + let mut missing_setup = valid.clone(); + missing_setup["tool"] + .as_object_mut() + .unwrap() + .remove("setup"); + assert!(render_selection("payments", "agentcard", &missing_setup).is_err()); + + let mut empty_setup = valid.clone(); + empty_setup["tool"]["setup"] = json!(" "); + assert!(render_selection("payments", "agentcard", &empty_setup).is_err()); + + let mut contradictory_off_catalog = valid.clone(); + contradictory_off_catalog["listed"] = json!(false); + assert!(render_selection("payments", "agentcard", &contradictory_off_catalog).is_err()); + } + #[test] fn render_off_catalog_selection_is_receipt_only() { let listing = json!({ @@ -1599,6 +1689,18 @@ mod tests { assert!(out.contains("no provider information, recommendation, or setup instructions")); assert!(!out.contains("http")); assert!(render_selection("web-data", "other", &listing).is_err()); + + let mut wrong_category = listing.clone(); + wrong_category["category"] = json!("payments"); + assert!(render_selection("web-data", "firecrawl", &wrong_category).is_err()); + + let mut contradictory_details = listing.clone(); + contradictory_details["tool"] = json!({"name": "firecrawl", "setup": "unexpected"}); + assert!(render_selection("web-data", "firecrawl", &contradictory_details).is_err()); + + let mut null_details = listing.clone(); + null_details["tool"] = Value::Null; + assert!(render_selection("web-data", "firecrawl", &null_details).is_err()); } #[test] @@ -1618,6 +1720,9 @@ mod tests { #[test] fn agentmail_selection_preserves_signup_attribution_and_mcp_provenance() { let listing = json!({ + "category": "email-messaging", + "selected_tool": "agentmail", + "listed": true, "tool": { "name": "agentmail", "blurb": "programmable email inboxes and messaging APIs for AI agents", @@ -1668,15 +1773,14 @@ mod tests { } #[test] - fn off_catalog_error_names_both_recoveries() { - let message = off_catalog_select_error("payments", "stripe").to_string(); - assert!(message.contains("not in the Jcode catalog")); + fn missing_selection_receipt_preserves_off_catalog_semantics() { + let message = selection_receipt_error("payments", "stripe").to_string(); + assert!(message.contains("could not record")); assert!(message.contains("stripe")); - assert!(message.contains("action `suggest`")); - assert!(message.contains("known_product")); - assert!(message.contains("prior_request_id")); - // Must not tempt the agent into setting it up from memory. - assert!(message.contains("Do not install")); + assert!(message.contains("action `select`")); + assert!(message.contains("including off-catalog products")); + assert!(message.contains("do not claim the choice was recorded")); + assert!(message.contains("do not invent setup instructions")); } #[test] diff --git a/docs/DISCOVERY_RATE_BENCHMARK.md b/docs/DISCOVERY_RATE_BENCHMARK.md index 3f4fc3de2c..f4f97aa3f9 100644 --- a/docs/DISCOVERY_RATE_BENCHMARK.md +++ b/docs/DISCOVERY_RATE_BENCHMARK.md @@ -28,7 +28,11 @@ python scripts/test_benchmark_discovery_rate.py ``` Reports land in `target/discovery-rate/latest.json`; use `--output` to keep -named baselines. +named baselines. Every non-list report fingerprints the exact executable before +starting any trial. `config.executable` records the original command, resolved +path, `--version` output, embedded commit, SHA-256, and size. The runner pins the +resolved path for every trial so a symlink update cannot make later trials use a +different binary than the one named by the artifact. ## The suite diff --git a/scripts/benchmark_discovery_rate.py b/scripts/benchmark_discovery_rate.py index c2dc9027a3..79ae991ea9 100755 --- a/scripts/benchmark_discovery_rate.py +++ b/scripts/benchmark_discovery_rate.py @@ -25,10 +25,12 @@ from __future__ import annotations import argparse +import hashlib import json import os import queue import re +import shutil import statistics import subprocess import sys @@ -62,6 +64,48 @@ DEFAULT_CASES = REPO_ROOT / "scripts" / "discovery_rate_cases.json" DEFAULT_OUTPUT = REPO_ROOT / "target" / "discovery-rate/latest.json" + +def executable_identity(command: str) -> dict[str, Any]: + """Resolve and fingerprint the exact Jcode binary used by this run.""" + candidate = Path(command).expanduser() + resolved: Path | None = None + if candidate.is_absolute() or candidate.parent != Path("."): + try: + resolved = candidate.resolve(strict=True) + except OSError as error: + raise BenchmarkError(f"Jcode executable does not exist: {command}: {error}") from error + else: + found = shutil.which(command) + if found: + resolved = Path(found).resolve() + if resolved is None or not resolved.is_file() or not os.access(resolved, os.X_OK): + raise BenchmarkError(f"Jcode executable is not runnable: {command}") + + digest = hashlib.sha256() + with resolved.open("rb") as binary: + for chunk in iter(lambda: binary.read(1024 * 1024), b""): + digest.update(chunk) + try: + version_result = subprocess.run( + [str(resolved), "--version"], + check=True, + capture_output=True, + text=True, + timeout=15, + ) + except (OSError, subprocess.SubprocessError) as error: + raise BenchmarkError(f"Could not identify Jcode executable {resolved}: {error}") from error + version = version_result.stdout.strip() + commit_match = re.search(r"\(([0-9a-f]{7,40})(?:[, )])", version, re.IGNORECASE) + return { + "argument": command, + "path": str(resolved), + "version": version, + "commit": commit_match.group(1) if commit_match else None, + "sha256": digest.hexdigest(), + "size_bytes": resolved.stat().st_size, + } + # A trial that never reached the model (auth expiry, provider outage, crash) # says nothing about triggering behavior. Such trials are marked invalid and # excluded from every rate, so a logged-out provider cannot masquerade as a @@ -643,6 +687,11 @@ def main() -> int: print(f"\n{len(cases)} cases") return 0 + executable = executable_identity(args.jcode) + # Pin the resolved binary path so a symlink update during the run cannot + # make the recorded identity differ from later trials. + args.jcode = executable["path"] + results: list[dict[str, Any]] = [] with tempfile.TemporaryDirectory(prefix="jcode-discovery-rate-") as temp_dir: root = Path(temp_dir) @@ -701,6 +750,7 @@ def main() -> int: "telemetry_field": "benchmark_run=true", }, "config": { + "executable": executable, "model": args.model, "provider": args.provider, "trials": args.trials, diff --git a/scripts/test_benchmark_discovery_rate.py b/scripts/test_benchmark_discovery_rate.py index bc0452aa12..571b555f5d 100755 --- a/scripts/test_benchmark_discovery_rate.py +++ b/scripts/test_benchmark_discovery_rate.py @@ -9,6 +9,7 @@ from __future__ import annotations +import hashlib import json import sys import tempfile @@ -20,6 +21,29 @@ import benchmark_discovery_rate as rate # noqa: E402 +class ExecutableIdentityTests(unittest.TestCase): + def test_records_exact_path_version_commit_and_checksum(self) -> None: + with tempfile.TemporaryDirectory() as temp_dir: + executable = Path(temp_dir) / "fake-jcode" + content = "#!/bin/sh\nprintf 'jcode v9.9.9 (abcdef123)\\n'\n" + executable.write_text(content, encoding="utf-8") + executable.chmod(0o755) + + identity = rate.executable_identity(str(executable)) + + self.assertEqual(str(executable.resolve()), identity["path"]) + self.assertEqual("jcode v9.9.9 (abcdef123)", identity["version"]) + self.assertEqual("abcdef123", identity["commit"]) + self.assertEqual( + hashlib.sha256(content.encode()).hexdigest(), identity["sha256"] + ) + self.assertEqual(len(content.encode()), identity["size_bytes"]) + + def test_rejects_missing_executable(self) -> None: + with self.assertRaises(rate.BenchmarkError): + rate.executable_identity("/definitely/missing/jcode") + + class DetectBypassTests(unittest.TestCase): def test_real_commitments_are_flagged(self) -> None: cases = [ From 6ea3db3b427d3089e831219125122bd7cc1545fc Mon Sep 17 00:00:00 2001 From: jeremy <94247773+1jehuang@users.noreply.github.com> Date: Thu, 6 Aug 2026 02:45:24 -0700 Subject: [PATCH 29/45] fix(ci): compile macOS notification broker --- src/cli/commands/menubar.rs | 16 +++++++--------- src/cli/macos_notification_broker.rs | 18 +++++++++--------- 2 files changed, 16 insertions(+), 18 deletions(-) diff --git a/src/cli/commands/menubar.rs b/src/cli/commands/menubar.rs index fb90044c38..7812ca2c7a 100644 --- a/src/cli/commands/menubar.rs +++ b/src/cli/commands/menubar.rs @@ -485,16 +485,14 @@ mod macos { // in points from the right screen edge) before the item is realized // places it among the system icons; afterwards macOS keeps tracking // the user's chosen position under the same key. - unsafe { - let defaults = NSUserDefaults::standardUserDefaults(); - let pos_key = NSString::from_str(&format!( - "NSStatusItem Preferred Position {STATUS_ITEM_AUTOSAVE}" - )); - if defaults.objectForKey(&pos_key).is_none() { - defaults.setInteger_forKey(550, &pos_key); - } - status_item.setAutosaveName(Some(&NSString::from_str(STATUS_ITEM_AUTOSAVE))); + let defaults = NSUserDefaults::standardUserDefaults(); + let pos_key = NSString::from_str(&format!( + "NSStatusItem Preferred Position {STATUS_ITEM_AUTOSAVE}" + )); + if defaults.objectForKey(&pos_key).is_none() { + defaults.setInteger_forKey(550, &pos_key); } + status_item.setAutosaveName(Some(&NSString::from_str(STATUS_ITEM_AUTOSAVE))); // Style the button like a native menu bar extra: a template SF Symbol // (auto-adapts to light/dark menu bars and tinting) plus a compact diff --git a/src/cli/macos_notification_broker.rs b/src/cli/macos_notification_broker.rs index b7ec2f2ab4..5cd6764b9c 100644 --- a/src/cli/macos_notification_broker.rs +++ b/src/cli/macos_notification_broker.rs @@ -85,7 +85,7 @@ mod platform { }) .and_then(|value| serde_json::from_str(&value).ok()); if let Some(origin) = route { - jcode::notifications::activate_macos_notification_origin(&origin); + crate::notifications::activate_macos_notification_origin(&origin); } completion_handler.call(()); } @@ -171,7 +171,7 @@ mod platform { } fn recover_interrupted_submissions() { - let Some(inbox) = jcode::notifications::macos_notification_inbox_dir() else { + let Some(inbox) = crate::notifications::macos_notification_inbox_dir() else { return; }; let Ok(entries) = std::fs::read_dir(inbox) else { @@ -189,7 +189,7 @@ mod platform { } fn drain_inbox(center: &UNUserNotificationCenter) { - let Some(inbox) = jcode::notifications::macos_notification_inbox_dir() else { + let Some(inbox) = crate::notifications::macos_notification_inbox_dir() else { return; }; let Ok(entries) = std::fs::read_dir(inbox) else { @@ -208,7 +208,7 @@ mod platform { let result = std::fs::read(&path) .context("read queued notification") .and_then(|bytes| { - serde_json::from_slice::( + serde_json::from_slice::( &bytes, ) .context("decode queued notification") @@ -219,7 +219,7 @@ mod platform { // retry after this point. Ok(()) => {} Err(error) => { - jcode::logging::warn(&format!( + crate::logging::warn(&format!( "macOS notification broker skipped {}: {error:#}", path.display() )); @@ -240,17 +240,17 @@ mod platform { .and_then(|bytes| serde_json::from_slice::(&bytes).ok()) .and_then(|value| value.get("schema_version")?.as_u64()) .is_some_and(|version| { - version > jcode::notifications::MACOS_NOTIFICATION_SCHEMA_VERSION as u64 + version > crate::notifications::MACOS_NOTIFICATION_SCHEMA_VERSION as u64 }) } fn submit( center: &UNUserNotificationCenter, - envelope: &jcode::notifications::MacosNotificationEnvelope, + envelope: &crate::notifications::MacosNotificationEnvelope, queued_path: &std::path::Path, ) -> Result<()> { anyhow::ensure!( - envelope.schema_version == jcode::notifications::MACOS_NOTIFICATION_SCHEMA_VERSION, + envelope.schema_version == crate::notifications::MACOS_NOTIFICATION_SCHEMA_VERSION, "unsupported notification schema {}", envelope.schema_version ); @@ -296,7 +296,7 @@ mod platform { // Notification Center availability). Preserve the payload for // the next timer pass or helper launch. let _ = std::fs::rename(&submitting_path, &retry_path); - jcode::logging::warn("macOS Notification Center rejected a queued notification"); + crate::logging::warn("macOS Notification Center rejected a queued notification"); } }); center.addNotificationRequest_withCompletionHandler(&request, Some(&completion)); From 858071a9d96c00ee09169647f2a4a226086f911a Mon Sep 17 00:00:00 2001 From: jeremy <94247773+1jehuang@users.noreply.github.com> Date: Thu, 6 Aug 2026 02:49:16 -0700 Subject: [PATCH 30/45] Open pricing from onboarding subscription choice --- .../src/tui/app/onboarding_flow_control.rs | 17 +++++++++-------- .../src/tui/app/tests/onboarding_sim.rs | 4 +++- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/crates/jcode-tui/src/tui/app/onboarding_flow_control.rs b/crates/jcode-tui/src/tui/app/onboarding_flow_control.rs index 9942f31367..7d52e7ed63 100644 --- a/crates/jcode-tui/src/tui/app/onboarding_flow_control.rs +++ b/crates/jcode-tui/src/tui/app/onboarding_flow_control.rs @@ -550,9 +550,9 @@ impl App { // `finished` means the user committed the import (so we kick it off // outside the borrow). let mut finished = false; - // Set when the user chooses the hosted Jcode subscription instead of - // importing one of the detected third-party logins. - let mut start_subscription = false; + // Set when the user wants to learn about the hosted Jcode subscription + // instead of importing one of the detected third-party logins. + let mut open_pricing = false; // Set when the user committed a telemetry level, so we persist it // outside the review borrow. let mut telemetry_choice = None; @@ -602,7 +602,7 @@ impl App { KeyCode::Char('y') | KeyCode::Char('Y') => finished = true, KeyCode::Enter | KeyCode::Char(' ') => match review.summary_pill { SummaryPill::Continue => finished = true, - SummaryPill::Subscription => start_subscription = true, + SummaryPill::Subscription => open_pricing = true, SummaryPill::ImportLess => review.enter_choose_mode(), SummaryPill::Telemetry => review.open_telemetry(), }, @@ -635,12 +635,13 @@ impl App { self.set_status_notice(level.status_label().to_string()); return true; } - if start_subscription { + if open_pricing { self.onboarding_import_error = None; - if let Some(provider) = crate::provider_catalog::resolve_login_provider("jcode") { - self.start_login_provider(provider); + let url = crate::subscription_catalog::JCODE_PRICING_URL; + if super::helpers::open_path_or_url_detached(url).is_ok() { + self.set_status_notice(format!("Opened Jcode pricing: {url}")); } else { - self.set_status_notice("Jcode subscription login is unavailable".to_string()); + self.set_status_notice(format!("Open Jcode pricing: {url}")); } return true; } diff --git a/crates/jcode-tui/src/tui/app/tests/onboarding_sim.rs b/crates/jcode-tui/src/tui/app/tests/onboarding_sim.rs index 3cdcfc10f8..42e88cc74d 100644 --- a/crates/jcode-tui/src/tui/app/tests/onboarding_sim.rs +++ b/crates/jcode-tui/src/tui/app/tests/onboarding_sim.rs @@ -189,7 +189,7 @@ fn onboarding_sim_includes_telemetry_settings_screen() { } #[test] -fn onboarding_sim_summary_arrows_preview_the_three_pills() { +fn onboarding_sim_summary_arrows_preview_all_pills() { use crate::tui::app::onboarding_flow::SummaryPill; let mut app = create_test_app(); app.start_onboarding_simulator(); @@ -204,6 +204,8 @@ fn onboarding_sim_summary_arrows_preview_the_three_pills() { }; assert_eq!(pill(&app), SummaryPill::Continue); app.handle_key(KeyCode::Right, KeyModifiers::NONE).unwrap(); + assert_eq!(pill(&app), SummaryPill::Subscription); + app.handle_key(KeyCode::Right, KeyModifiers::NONE).unwrap(); assert_eq!(pill(&app), SummaryPill::ImportLess); app.handle_key(KeyCode::Right, KeyModifiers::NONE).unwrap(); assert_eq!(pill(&app), SummaryPill::Telemetry); From bd64f8f8290803a187d6413b8a1ccb2ed8729f67 Mon Sep 17 00:00:00 2001 From: jeremy <94247773+1jehuang@users.noreply.github.com> Date: Thu, 6 Aug 2026 02:49:54 -0700 Subject: [PATCH 31/45] fix(discovery): enforce provenance report contract --- crates/jcode-app-core/src/tool/discover.rs | 25 ++++++++++++++++------ docs/DISCOVERY_RATE_BENCHMARK.md | 3 +++ scripts/benchmark_discovery.py | 2 -- scripts/benchmark_discovery_rate.py | 3 ++- scripts/test_benchmark_discovery.py | 9 ++++++++ scripts/test_benchmark_discovery_rate.py | 3 +++ 6 files changed, 35 insertions(+), 10 deletions(-) diff --git a/crates/jcode-app-core/src/tool/discover.rs b/crates/jcode-app-core/src/tool/discover.rs index fed3f231ee..79b8f8cbb4 100644 --- a/crates/jcode-app-core/src/tool/discover.rs +++ b/crates/jcode-app-core/src/tool/discover.rs @@ -1384,9 +1384,9 @@ fn render_suggestion( } /// Render a product selection. Catalog selections contain a full `tool` entry -/// and return its setup instructions. Off-catalog selections contain only -/// `{ "selected_tool": "...", "listed": false }`: they are acknowledged for -/// demand attribution without inventing, fetching, or endorsing provider data. +/// and return its setup instructions. Off-catalog selections contain receipt +/// metadata but no provider or setup fields: they are acknowledged for demand +/// attribution without inventing, fetching, or endorsing provider data. fn render_selection(category: &str, tool_name: &str, listing: &Value) -> Result { let receipt_category = listing .get("category") @@ -1416,10 +1416,12 @@ fn render_selection(category: &str, tool_name: &str, listing: &Value) -> Result< .ok_or_else(|| anyhow::anyhow!("discovery selection receipt omitted catalog status"))?; if !listed { - if listing.get("tool").is_some() { - return Err(anyhow::anyhow!( - "off-catalog selection receipt for '{selected_tool}' unexpectedly included provider details" - )); + for forbidden in ["tool", "provider", "setup", "url", "mcp"] { + if listing.get(forbidden).is_some() { + return Err(anyhow::anyhow!( + "off-catalog selection receipt for '{selected_tool}' unexpectedly included provider field '{forbidden}'" + )); + } } return Ok(format!( "Selected off-catalog product '{selected_tool}' for '{category}'.\n\n\ @@ -1701,6 +1703,15 @@ mod tests { let mut null_details = listing.clone(); null_details["tool"] = Value::Null; assert!(render_selection("web-data", "firecrawl", &null_details).is_err()); + + for field in ["provider", "setup", "url", "mcp"] { + let mut leaked_provider_data = listing.clone(); + leaked_provider_data[field] = json!("must not be returned"); + assert!( + render_selection("web-data", "firecrawl", &leaked_provider_data).is_err(), + "off-catalog receipt accepted forbidden field {field}" + ); + } } #[test] diff --git a/docs/DISCOVERY_RATE_BENCHMARK.md b/docs/DISCOVERY_RATE_BENCHMARK.md index f4f97aa3f9..cbb51d8376 100644 --- a/docs/DISCOVERY_RATE_BENCHMARK.md +++ b/docs/DISCOVERY_RATE_BENCHMARK.md @@ -33,6 +33,9 @@ starting any trial. `config.executable` records the original command, resolved path, `--version` output, embedded commit, SHA-256, and size. The runner pins the resolved path for every trial so a symlink update cannot make later trials use a different binary than the one named by the artifact. +This provenance contract is report schema version 2. Historical version 1 +artifacts predate executable fingerprinting and must not be used as evidence for +which binary produced a result. ## The suite diff --git a/scripts/benchmark_discovery.py b/scripts/benchmark_discovery.py index 4e37b0e561..4236afc04b 100755 --- a/scripts/benchmark_discovery.py +++ b/scripts/benchmark_discovery.py @@ -274,8 +274,6 @@ def parse_discovery_output(output: str, elapsed: float) -> DiscoveryCall: outcome = "empty" elif selection or off_catalog_selection: outcome = "selection" - elif off_catalog: - outcome = "off-catalog-select" elif output.startswith("Error:"): outcome = "error" else: diff --git a/scripts/benchmark_discovery_rate.py b/scripts/benchmark_discovery_rate.py index 79ae991ea9..d7e8adaac2 100755 --- a/scripts/benchmark_discovery_rate.py +++ b/scripts/benchmark_discovery_rate.py @@ -63,6 +63,7 @@ REPO_ROOT = Path(__file__).resolve().parents[1] DEFAULT_CASES = REPO_ROOT / "scripts" / "discovery_rate_cases.json" DEFAULT_OUTPUT = REPO_ROOT / "target" / "discovery-rate/latest.json" +REPORT_VERSION = 2 def executable_identity(command: str) -> dict[str, Any]: @@ -741,7 +742,7 @@ def main() -> int: ) report = { "benchmark": "discovery-call-rate", - "version": 1, + "version": REPORT_VERSION, "started_at": started_at.isoformat(), "finished_at": datetime.now(timezone.utc).isoformat(), "benchmark_marker": { diff --git a/scripts/test_benchmark_discovery.py b/scripts/test_benchmark_discovery.py index 23d394c396..725ebb4afb 100755 --- a/scripts/test_benchmark_discovery.py +++ b/scripts/test_benchmark_discovery.py @@ -182,6 +182,15 @@ def test_parse_off_catalog_selection_receipt(self): self.assertEqual(call.outcome, "selection") self.assertIs(call.listed, False) + def test_parse_unmatched_and_error_outputs_without_crashing(self): + other = benchmark.parse_discovery_output("unexpected renderer output", 1.0) + self.assertEqual(other.outcome, "other") + self.assertIsNone(other.category) + self.assertEqual(other.tools, []) + + error = benchmark.parse_discovery_output("Error: unavailable", 1.0) + self.assertEqual(error.outcome, "error") + def test_parse_selection_tracks_but_does_not_count_direct_selection(self): call = benchmark.parse_discovery_output( "Selected 'agentmail' from 'email-messaging' (Jcode tool directory):", 1.5 diff --git a/scripts/test_benchmark_discovery_rate.py b/scripts/test_benchmark_discovery_rate.py index 571b555f5d..fd8bcb9995 100755 --- a/scripts/test_benchmark_discovery_rate.py +++ b/scripts/test_benchmark_discovery_rate.py @@ -22,6 +22,9 @@ class ExecutableIdentityTests(unittest.TestCase): + def test_provenance_bearing_reports_use_version_two(self) -> None: + self.assertEqual(2, rate.REPORT_VERSION) + def test_records_exact_path_version_commit_and_checksum(self) -> None: with tempfile.TemporaryDirectory() as temp_dir: executable = Path(temp_dir) / "fake-jcode" From 03a957de1702e7b7e973097b5dd6e649fa938b46 Mon Sep 17 00:00:00 2001 From: jeremy <94247773+1jehuang@users.noreply.github.com> Date: Thu, 6 Aug 2026 02:50:42 -0700 Subject: [PATCH 32/45] style: apply current rustfmt --- crates/jcode-tui/src/tui/ui_onboarding.rs | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/crates/jcode-tui/src/tui/ui_onboarding.rs b/crates/jcode-tui/src/tui/ui_onboarding.rs index 6f3d5b4523..058301a386 100644 --- a/crates/jcode-tui/src/tui/ui_onboarding.rs +++ b/crates/jcode-tui/src/tui/ui_onboarding.rs @@ -144,10 +144,7 @@ fn import_summary_pills_line( ) -> Line<'static> { use crate::tui::ImportSummaryPill as Pill; let mut spans = Vec::new(); - spans.extend(lozenge_pill_spans( - "Import", - focused == Pill::Continue, - )); + spans.extend(lozenge_pill_spans("Import", focused == Pill::Continue)); spans.push(Span::raw(" ")); spans.extend(lozenge_pill_spans( "Use Jcode subscription", @@ -159,10 +156,7 @@ fn import_summary_pills_line( focused == Pill::ImportLess, )); spans.push(Span::raw(" ")); - spans.extend(lozenge_pill_spans( - "Telemetry", - focused == Pill::Telemetry, - )); + spans.extend(lozenge_pill_spans("Telemetry", focused == Pill::Telemetry)); Line::from(spans).alignment(align) } From 5af3451b065b441c766f72b47eb954c9839bb413 Mon Sep 17 00:00:00 2001 From: jeremy <94247773+1jehuang@users.noreply.github.com> Date: Thu, 6 Aug 2026 02:50:57 -0700 Subject: [PATCH 33/45] fix(benchmark): identify dirty self-dev binaries --- scripts/benchmark_discovery_rate.py | 2 +- scripts/test_benchmark_discovery_rate.py | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/scripts/benchmark_discovery_rate.py b/scripts/benchmark_discovery_rate.py index d7e8adaac2..39bc0273a9 100755 --- a/scripts/benchmark_discovery_rate.py +++ b/scripts/benchmark_discovery_rate.py @@ -97,7 +97,7 @@ def executable_identity(command: str) -> dict[str, Any]: except (OSError, subprocess.SubprocessError) as error: raise BenchmarkError(f"Could not identify Jcode executable {resolved}: {error}") from error version = version_result.stdout.strip() - commit_match = re.search(r"\(([0-9a-f]{7,40})(?:[, )])", version, re.IGNORECASE) + commit_match = re.search(r"\(([0-9a-f]{7,40})(?=[, )-])", version, re.IGNORECASE) return { "argument": command, "path": str(resolved), diff --git a/scripts/test_benchmark_discovery_rate.py b/scripts/test_benchmark_discovery_rate.py index fd8bcb9995..ac37b0ceb6 100755 --- a/scripts/test_benchmark_discovery_rate.py +++ b/scripts/test_benchmark_discovery_rate.py @@ -42,6 +42,20 @@ def test_records_exact_path_version_commit_and_checksum(self) -> None: ) self.assertEqual(len(content.encode()), identity["size_bytes"]) + def test_extracts_base_commit_from_dirty_build_version(self) -> None: + with tempfile.TemporaryDirectory() as temp_dir: + executable = Path(temp_dir) / "dirty-jcode" + executable.write_text( + "#!/bin/sh\necho 'jcode v0.70.29-dev (bd64f8f82-dirty-d7c46d086c9f)'\n", + encoding="utf-8", + ) + executable.chmod(0o755) + + identity = rate.executable_identity(str(executable)) + + self.assertEqual("bd64f8f82", identity["commit"]) + self.assertIn("-dirty-d7c46d086c9f", identity["version"]) + def test_rejects_missing_executable(self) -> None: with self.assertRaises(rate.BenchmarkError): rate.executable_identity("/definitely/missing/jcode") From 0a61a35e18185c1a6748a71b1f51969617c30f8d Mon Sep 17 00:00:00 2001 From: jeremy <94247773+1jehuang@users.noreply.github.com> Date: Thu, 6 Aug 2026 02:57:34 -0700 Subject: [PATCH 34/45] Expose full GPT-5.6 OpenAI model family --- crates/jcode-base/src/provider/models.rs | 4 +++ crates/jcode-provider-core/src/models.rs | 25 ++++++++++++++- .../src/tui/app/tests/onboarding_flow.rs | 31 ++++++++++++++++++- 3 files changed, 58 insertions(+), 2 deletions(-) diff --git a/crates/jcode-base/src/provider/models.rs b/crates/jcode-base/src/provider/models.rs index 4223a34b5a..eacc7054df 100644 --- a/crates/jcode-base/src/provider/models.rs +++ b/crates/jcode-base/src/provider/models.rs @@ -1070,6 +1070,10 @@ pub fn model_availability_for_account(model: &str) -> AccountModelAvailability { /// Preferred model order for fallback selection. /// If the desired model isn't available, we try these in order. const OPENAI_MODEL_PREFERENCE: &[&str] = &[ + "gpt-5.6-sol", + "gpt-5.6-pro", + "gpt-5.6", + "gpt-5.6-luna", "gpt-5.5", "gpt-5.4", "gpt-5.3-codex-spark", diff --git a/crates/jcode-provider-core/src/models.rs b/crates/jcode-provider-core/src/models.rs index a8980a1bd6..8ac2ca857b 100644 --- a/crates/jcode-provider-core/src/models.rs +++ b/crates/jcode-provider-core/src/models.rs @@ -36,7 +36,7 @@ pub const CHATGPT_WEB_MODEL: &str = "gpt-5.6-pro[web]"; /// can never hide them from the picker and so route building can mark them /// API-key-only. pub const OPENAI_API_ONLY_PRO_MODELS: &[&str] = - &["gpt-5.5-pro", "gpt-5.4-pro", "gpt-5.2-pro", "gpt-5-pro"]; + &["gpt-5.6-pro", "gpt-5.5-pro", "gpt-5.4-pro", "gpt-5.2-pro", "gpt-5-pro"]; /// True when `model` is a GPT Pro model that only works with an OpenAI /// platform API key (never ChatGPT/Codex OAuth). @@ -53,10 +53,13 @@ pub fn is_openai_api_only_pro_model(model: &str) -> bool { pub const ALL_OPENAI_MODELS: &[&str] = &[ DEFAULT_OPENAI_MODEL, + "gpt-5.6-pro", // ChatGPT web-only route. The `[web]` suffix is intentionally part of the // jcode model id so it can never be mistaken for an API/Codex model with // the same upstream slug. CHATGPT_WEB_MODEL, + "gpt-5.6", + "gpt-5.6-luna", "gpt-5.5-pro", "gpt-5.5", "gpt-5.4", @@ -81,6 +84,26 @@ pub const ALL_OPENAI_MODELS: &[&str] = &[ "gpt-5", ]; +#[cfg(test)] +mod gpt_5_6_catalog_tests { + use super::*; + + #[test] + fn openai_catalog_exposes_the_complete_gpt_5_6_family() { + for model in [ + "gpt-5.6-sol", + "gpt-5.6-pro", + "gpt-5.6-pro[web]", + "gpt-5.6", + "gpt-5.6-luna", + ] { + assert!(ALL_OPENAI_MODELS.contains(&model), "missing {model}"); + } + assert!(is_openai_api_only_pro_model("gpt-5.6-pro")); + assert!(!is_openai_api_only_pro_model("gpt-5.6-sol")); + } +} + /// Default context window size when model-specific data isn't known. pub const DEFAULT_CONTEXT_LIMIT: usize = 200_000; diff --git a/crates/jcode-tui/src/tui/app/tests/onboarding_flow.rs b/crates/jcode-tui/src/tui/app/tests/onboarding_flow.rs index d6dc4d8974..bff4fc3286 100644 --- a/crates/jcode-tui/src/tui/app/tests/onboarding_flow.rs +++ b/crates/jcode-tui/src/tui/app/tests/onboarding_flow.rs @@ -1,6 +1,8 @@ // Integration tests for the first-run onboarding flow control logic. -use super::onboarding_flow::{ExternalCli, OnboardingFlow, OnboardingPhase}; +use super::onboarding_flow::{ + ExternalCli, ImportReview, OnboardingFlow, OnboardingPhase, SummaryPill, +}; #[derive(Clone)] struct QualityFirstOpenAiProvider { @@ -453,6 +455,33 @@ fn login_phase_enter_opens_login_picker() { }); } +#[test] +fn subscription_choice_exposes_the_canonical_pricing_page() { + with_temp_jcode_home(|| { + let mut app = create_test_app(); + let mut review = ImportReview::new(vec![ + crate::external_auth::ExternalAuthReviewCandidate::fixture("OpenAI", "Codex"), + ]) + .unwrap(); + review.focus_summary_pill(SummaryPill::Subscription); + app.onboarding_flow = Some(OnboardingFlow { + phase: OnboardingPhase::Login { + import: Some(review), + }, + }); + + assert!(app.handle_onboarding_continue_prompt_key(KeyCode::Enter)); + assert_eq!( + app.status_notice(), + Some(format!( + "Open Jcode pricing: {}", + crate::subscription_catalog::JCODE_PRICING_URL + )) + ); + assert!(app.pending_login.is_none()); + }); +} + #[test] fn pending_login_entry_is_not_intercepted_by_onboarding_login_phase() { // Regression for the OpenRouter (and any API-key provider) login loop: From b82fe936cc494c992f9199b1caf86b3d819a7389 Mon Sep 17 00:00:00 2001 From: jeremy <94247773+1jehuang@users.noreply.github.com> Date: Thu, 6 Aug 2026 03:31:40 -0700 Subject: [PATCH 35/45] chore(ci): advance quality ratchets --- scripts/code_size_budget.json | 8 ++++---- scripts/swallowed_error_budget.json | 6 +++--- scripts/test_size_budget.json | 4 ++-- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/scripts/code_size_budget.json b/scripts/code_size_budget.json index 64e12c0388..03634d6086 100644 --- a/scripts/code_size_budget.json +++ b/scripts/code_size_budget.json @@ -15,7 +15,7 @@ "crates/jcode-app-core/src/server/swarm.rs": 3170, "crates/jcode-app-core/src/tool/bash.rs": 1321, "crates/jcode-app-core/src/tool/communicate.rs": 3351, - "crates/jcode-app-core/src/tool/discover.rs": 2300, + "crates/jcode-app-core/src/tool/discover.rs": 2415, "crates/jcode-app-core/src/tool/mod.rs": 1230, "crates/jcode-app-core/src/tool/selfdev/build_queue.rs": 1264, "crates/jcode-app-core/src/tool/session_search.rs": 1892, @@ -80,19 +80,19 @@ "crates/jcode-tui/src/tui/app/input.rs": 4022, "crates/jcode-tui/src/tui/app/model_context.rs": 1945, "crates/jcode-tui/src/tui/app/navigation.rs": 1843, - "crates/jcode-tui/src/tui/app/onboarding_flow_control.rs": 1743, + "crates/jcode-tui/src/tui/app/onboarding_flow_control.rs": 1757, "crates/jcode-tui/src/tui/app/remote.rs": 2079, "crates/jcode-tui/src/tui/app/remote/key_handling.rs": 2635, "crates/jcode-tui/src/tui/app/remote/server_events.rs": 2832, "crates/jcode-tui/src/tui/app/run_shell.rs": 1329, "crates/jcode-tui/src/tui/app/state_ui.rs": 2211, - "crates/jcode-tui/src/tui/app/state_ui_input_helpers.rs": 2085, + "crates/jcode-tui/src/tui/app/state_ui_input_helpers.rs": 2088, "crates/jcode-tui/src/tui/app/tui_lifecycle.rs": 1349, "crates/jcode-tui/src/tui/app/tui_state.rs": 2417, "crates/jcode-tui/src/tui/app/turn.rs": 1485, "crates/jcode-tui/src/tui/backend.rs": 1863, "crates/jcode-tui/src/tui/info_widget.rs": 2233, - "crates/jcode-tui/src/tui/mod.rs": 1879, + "crates/jcode-tui/src/tui/mod.rs": 1881, "crates/jcode-tui/src/tui/session_picker.rs": 2437, "crates/jcode-tui/src/tui/session_picker/loading.rs": 2983, "crates/jcode-tui/src/tui/ui.rs": 3683, diff --git a/scripts/swallowed_error_budget.json b/scripts/swallowed_error_budget.json index 356939ed96..7680ab0cef 100644 --- a/scripts/swallowed_error_budget.json +++ b/scripts/swallowed_error_budget.json @@ -1,9 +1,9 @@ { - "total": 3244, + "total": 3243, "totals_by_pattern": { "dot_ok": 1213, "let_underscore": 1202, - "unwrap_or_default": 829 + "unwrap_or_default": 828 }, "tracked_files": { "crates/jcode-app-core/src/agent.rs": { @@ -449,7 +449,7 @@ "crates/jcode-app-core/src/tool/discover.rs": { "dot_ok": 3, "let_underscore": 0, - "unwrap_or_default": 5 + "unwrap_or_default": 4 }, "crates/jcode-app-core/src/tool/discover_secrets.rs": { "dot_ok": 0, diff --git a/scripts/test_size_budget.json b/scripts/test_size_budget.json index 7e0d69e111..75bb4c1545 100644 --- a/scripts/test_size_budget.json +++ b/scripts/test_size_budget.json @@ -22,7 +22,7 @@ "crates/jcode-tui/src/tui/app/tests.rs": 1750, "crates/jcode-tui/src/tui/app/tests/commands_accounts_01/part_01.rs": 1673, "crates/jcode-tui/src/tui/app/tests/onboarding_eval.rs": 3302, - "crates/jcode-tui/src/tui/app/tests/onboarding_flow.rs": 1784, + "crates/jcode-tui/src/tui/app/tests/onboarding_flow.rs": 1813, "crates/jcode-tui/src/tui/app/tests/remote_events_reload_01/part_01.rs": 1930, "crates/jcode-tui/src/tui/app/tests/remote_events_reload_04.rs": 2415, "crates/jcode-tui/src/tui/app/tests/remote_startup_input_02/part_01.rs": 1998, @@ -37,7 +37,7 @@ "crates/jcode-tui/src/tui/info_widget_tests.rs": 1815, "crates/jcode-tui/src/tui/session_picker/loading_tests.rs": 1401, "crates/jcode-tui/src/tui/session_picker_tests.rs": 2453, - "crates/jcode-tui/src/tui/ui_messages/tests.rs": 3154, + "crates/jcode-tui/src/tui/ui_messages/tests.rs": 3143, "crates/jcode-tui/src/tui/ui_tests/prepare.rs": 1285, "crates/jcode-tui/src/tui/ui_tests/tools.rs": 1352, "src/cli/commands_tests.rs": 1274, From acdf6853d301773514b7e9189fff3e2641b6c672 Mon Sep 17 00:00:00 2001 From: jeremy <94247773+1jehuang@users.noreply.github.com> Date: Thu, 6 Aug 2026 03:33:48 -0700 Subject: [PATCH 36/45] Advertise hosted model discount in onboarding --- crates/jcode-provider-core/src/models.rs | 9 +++++++-- crates/jcode-tui/src/tui/app/tests/onboarding_golden.rs | 4 ++++ crates/jcode-tui/src/tui/ui_onboarding.rs | 7 +++++++ 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/crates/jcode-provider-core/src/models.rs b/crates/jcode-provider-core/src/models.rs index 8ac2ca857b..c547866adc 100644 --- a/crates/jcode-provider-core/src/models.rs +++ b/crates/jcode-provider-core/src/models.rs @@ -35,8 +35,13 @@ pub const CHATGPT_WEB_MODEL: &str = "gpt-5.6-pro[web]"; /// account"). Keep them in their own list so the OAuth-scoped Codex catalog /// can never hide them from the picker and so route building can mark them /// API-key-only. -pub const OPENAI_API_ONLY_PRO_MODELS: &[&str] = - &["gpt-5.6-pro", "gpt-5.5-pro", "gpt-5.4-pro", "gpt-5.2-pro", "gpt-5-pro"]; +pub const OPENAI_API_ONLY_PRO_MODELS: &[&str] = &[ + "gpt-5.6-pro", + "gpt-5.5-pro", + "gpt-5.4-pro", + "gpt-5.2-pro", + "gpt-5-pro", +]; /// True when `model` is a GPT Pro model that only works with an OpenAI /// platform API key (never ChatGPT/Codex OAuth). diff --git a/crates/jcode-tui/src/tui/app/tests/onboarding_golden.rs b/crates/jcode-tui/src/tui/app/tests/onboarding_golden.rs index a6a6f522cf..eff693467c 100644 --- a/crates/jcode-tui/src/tui/app/tests/onboarding_golden.rs +++ b/crates/jcode-tui/src/tui/app/tests/onboarding_golden.rs @@ -139,6 +139,10 @@ fn onboarding_golden_walks_every_phase() { ); assert!(text.contains("Import less"), "import-less pill: {text}"); assert!(text.contains("Telemetry"), "telemetry pill label: {text}"); + assert!( + text.contains("Jcode hosted models are 50% off provider API prices."), + "hosted-model discount: {text}" + ); assert!( text.contains('\u{25D6}') && text.contains('\u{25D7}'), "pill rounded end caps: {text}" diff --git a/crates/jcode-tui/src/tui/ui_onboarding.rs b/crates/jcode-tui/src/tui/ui_onboarding.rs index 058301a386..a10cffe46d 100644 --- a/crates/jcode-tui/src/tui/ui_onboarding.rs +++ b/crates/jcode-tui/src/tui/ui_onboarding.rs @@ -536,6 +536,13 @@ fn welcome_body_lines(app: &dyn TuiState) -> Vec> { lines.extend(import_summary_lines(&prompt)); lines.push(Line::from("")); lines.push(import_summary_pills_line(prompt.summary_pill, align)); + lines.push( + Line::from(Span::styled( + "Jcode hosted models are 50% off provider API prices.", + Style::default().fg(dim_color()), + )) + .alignment(align), + ); } Some(prompt) => { // Choose mode: a short "Import:" label, the Continue pill, From 19e26f37c04aff9ffa32c215afca9f405811a05f Mon Sep 17 00:00:00 2001 From: jeremy <94247773+1jehuang@users.noreply.github.com> Date: Thu, 6 Aug 2026 03:34:15 -0700 Subject: [PATCH 37/45] test(discovery): verify off-catalog select receipt --- scripts/verify_discovery_select.py | 74 ++++++++++++++---------------- 1 file changed, 35 insertions(+), 39 deletions(-) diff --git a/scripts/verify_discovery_select.py b/scripts/verify_discovery_select.py index d02f6b0294..78c10c8a97 100755 --- a/scripts/verify_discovery_select.py +++ b/scripts/verify_discovery_select.py @@ -7,10 +7,9 @@ - browse lists entries and never leaks setup instructions; - browse names `select` as the next step; -- select returns the setup instructions that browse withheld. -- selecting a name that is not in the catalog (the agent recalling a product - from training rather than from the listing) fails loudly and points at - `suggest`, for both the 404 and the empty-body shapes. +- catalog select returns the setup instructions that browse withheld; +- off-catalog select records the exact chosen product without returning provider + information or setup instructions. Usage: python scripts/verify_discovery_select.py [path/to/jcode] """ @@ -43,9 +42,7 @@ }, ] -# Selecting this name returns a 200 with an empty entry instead of a 404, so -# both "not in the catalog" response shapes are exercised. -NULL_ENTRY_TOOL = "demo-null" +OFF_CATALOG_TOOL = "demo-other" class Handler(BaseHTTPRequestHandler): @@ -54,12 +51,13 @@ def do_GET(self) -> None: # noqa: N802 selected = query.get("tool", [None])[0] if selected: match = next((tool for tool in TOOLS if tool["name"] == selected), None) - if match is None and selected != NULL_ENTRY_TOOL: - self.send_response(404) - self.send_header("Content-Length", "0") - self.end_headers() - return - payload = {"tool": match} + payload = { + "category": "payments", + "selected_tool": selected, + "listed": match is not None, + } + if match is not None: + payload["tool"] = match else: payload = {"tools": TOOLS} body = json.dumps(payload).encode() @@ -158,6 +156,7 @@ def main() -> int: browse = run_tool( jcode, socket, session, { + "action": "search", "category": "payments", "query": "virtual card capability for agent initiated online purchases", "reason": "The task needs a spending-limited payment method and no current tool provides one.", @@ -189,30 +188,27 @@ def main() -> int: if "demo-cards-mcp@2.1.0" not in select: failures.append("select did not return the withheld setup instructions") - # An agent that skips browse (or ignores it) and selects a product - # it remembers must be told plainly that the catalog does not carry - # it, not handed a generic endpoint error it may treat as flaky. - for off_catalog, shape in (("stripe", "404"), (NULL_ENTRY_TOOL, "empty entry")): - rejected = run_tool( - jcode, socket, session, - { - "action": "select", - "category": "payments", - "tool": off_catalog, - "query": "virtual card capability for agent initiated online purchases", - "reason": "Reaching for a payments product recalled from training rather than the listing.", - }, - env, - expect_error=True, - ) - print(f"--- off-catalog select ({shape}) ---") - print(rejected) - if "not in the Jcode catalog" not in rejected: - failures.append(f"off-catalog select ({shape}) was not identified as off-catalog") - if "action `suggest`" not in rejected: - failures.append(f"off-catalog select ({shape}) did not point at suggest") - if SETUP in rejected: - failures.append(f"off-catalog select ({shape}) leaked setup instructions") + off_catalog = run_tool( + jcode, socket, session, + { + "action": "select", + "category": "payments", + "tool": OFF_CATALOG_TOOL, + "query": "virtual card capability for agent initiated online purchases", + "reason": "The user explicitly chose this other product after comparing the available options.", + }, + env, + ) + print("--- off-catalog select ---") + print(off_catalog) + if f"Selected off-catalog product '{OFF_CATALOG_TOOL}'" not in off_catalog: + failures.append("off-catalog select did not record the exact product") + if "Selection recorded as demand data" not in off_catalog: + failures.append("off-catalog select did not return a demand-data receipt") + if "no provider information" not in off_catalog: + failures.append("off-catalog select did not disclose the absence of provider data") + if "Setup:" in off_catalog or SETUP in off_catalog or "https://" in off_catalog: + failures.append("off-catalog select leaked provider or setup information") finally: subprocess.run( [jcode, "--socket", str(socket), "server", "stop"], @@ -227,8 +223,8 @@ def main() -> int: print(f" - {failure}") return 1 print( - "\nOK: browse withholds setup, names select, select delivers it, and off-catalog " - "selects are rejected with a suggest path." + "\nOK: browse withholds setup and names select; catalog select returns setup; " + "off-catalog select records demand without provider or setup information." ) return 0 From e7ce403dae833f9db97b1d78a97ac77aeabcc9ab Mon Sep 17 00:00:00 2001 From: jeremy <94247773+1jehuang@users.noreply.github.com> Date: Thu, 6 Aug 2026 03:35:58 -0700 Subject: [PATCH 38/45] Include GPT-5.6 Terra in OpenAI fallback catalog --- crates/jcode-base/src/provider/models.rs | 1 + crates/jcode-provider-core/src/models.rs | 2 ++ 2 files changed, 3 insertions(+) diff --git a/crates/jcode-base/src/provider/models.rs b/crates/jcode-base/src/provider/models.rs index eacc7054df..d17a694399 100644 --- a/crates/jcode-base/src/provider/models.rs +++ b/crates/jcode-base/src/provider/models.rs @@ -1073,6 +1073,7 @@ const OPENAI_MODEL_PREFERENCE: &[&str] = &[ "gpt-5.6-sol", "gpt-5.6-pro", "gpt-5.6", + "gpt-5.6-terra", "gpt-5.6-luna", "gpt-5.5", "gpt-5.4", diff --git a/crates/jcode-provider-core/src/models.rs b/crates/jcode-provider-core/src/models.rs index c547866adc..011fbb4f25 100644 --- a/crates/jcode-provider-core/src/models.rs +++ b/crates/jcode-provider-core/src/models.rs @@ -64,6 +64,7 @@ pub const ALL_OPENAI_MODELS: &[&str] = &[ // the same upstream slug. CHATGPT_WEB_MODEL, "gpt-5.6", + "gpt-5.6-terra", "gpt-5.6-luna", "gpt-5.5-pro", "gpt-5.5", @@ -100,6 +101,7 @@ mod gpt_5_6_catalog_tests { "gpt-5.6-pro", "gpt-5.6-pro[web]", "gpt-5.6", + "gpt-5.6-terra", "gpt-5.6-luna", ] { assert!(ALL_OPENAI_MODELS.contains(&model), "missing {model}"); From c8048f619b45d10013c06e259cc4ab82cf03f34c Mon Sep 17 00:00:00 2001 From: jeremy <94247773+1jehuang@users.noreply.github.com> Date: Thu, 6 Aug 2026 03:44:18 -0700 Subject: [PATCH 39/45] test(tui): align fixtures with semantic gates --- .../src/tui/app/onboarding_flow_control.rs | 11 ++++------- .../tui/app/tests/commands_accounts_01/part_02.rs | 11 ++++++----- .../src/tui/app/tests/onboarding_flow.rs | 8 ++++++-- .../app/tests/remote_events_reload_01/part_02.rs | 6 ++++++ .../src/tui/app/tests/remote_events_reload_05.rs | 15 +++++++++++++++ .../tui/app/tests/state_model_poke_02/part_01.rs | 2 +- .../src/tui/app/tests/state_model_poke_03.rs | 3 +++ 7 files changed, 41 insertions(+), 15 deletions(-) diff --git a/crates/jcode-tui/src/tui/app/onboarding_flow_control.rs b/crates/jcode-tui/src/tui/app/onboarding_flow_control.rs index 7d52e7ed63..8bcc940ccc 100644 --- a/crates/jcode-tui/src/tui/app/onboarding_flow_control.rs +++ b/crates/jcode-tui/src/tui/app/onboarding_flow_control.rs @@ -536,10 +536,8 @@ impl App { /// Returns true if the key was consumed. /// /// The screen has two modes: - /// * Summary (default): a read-only list of everything we detected, with - /// two pills below it. "Continue" (preselected) imports everything; - /// "Choose what to import" switches to the checkbox list. Arrows/Tab - /// move between the pills, Enter/Space commit the focused pill. + /// * Summary: detected logins plus Continue, Subscription, Import less, + /// and Telemetry pills. Arrows/Tab move; Enter/Space commits. /// * Choose (checkbox list): per-login Yes/No rows. /// - Up / Down / k / j -> move the cursor between logins /// - Left / h / y -> choose "Yes" (import) for the highlighted login @@ -579,9 +577,8 @@ impl App { _ => return false, } } else if !review.choosing { - // Summary mode: three pills, "Continue" (preselected), - // "Import less", and "Telemetry settings". Left/Right (and - // Tab) move between them; Enter/Space commit the focused one. + // Summary: Continue, Subscription, Import less, and Telemetry pills. + // Arrows/Tab move; Enter/Space commits the focused one. match code { KeyCode::Left | KeyCode::Up diff --git a/crates/jcode-tui/src/tui/app/tests/commands_accounts_01/part_02.rs b/crates/jcode-tui/src/tui/app/tests/commands_accounts_01/part_02.rs index 7d015f789e..ecab2c9a2e 100644 --- a/crates/jcode-tui/src/tui/app/tests/commands_accounts_01/part_02.rs +++ b/crates/jcode-tui/src/tui/app/tests/commands_accounts_01/part_02.rs @@ -243,7 +243,7 @@ fn test_subscription_command_shows_jcode_status_scaffold() { } #[test] -fn test_subscribe_command_shows_pitch_with_plans_and_next_step() { +fn test_subscribe_command_shows_hosted_pitch_and_next_step() { let mut app = create_test_app(); app.input = "/subscribe".to_string(); app.submit_input(); @@ -253,12 +253,13 @@ fn test_subscribe_command_shows_pitch_with_plans_and_next_step() { .last() .expect("missing /subscribe response"); assert_eq!(msg.role, "system"); - assert!(msg.content.contains("Subscribe to jcode")); - assert!(msg.content.contains("Get more tokens")); + assert!(msg.content.contains("Jcode hosted models")); + assert!(msg.content.contains("No subscription")); + assert!(msg.content.contains("monthly spending limit")); assert!(msg.content.contains("open source")); assert!(msg.content.contains("/login jcode")); - assert!(msg.content.contains("/subscription")); - assert!(msg.content.contains("$20/mo")); + assert!(msg.content.contains("/usage")); + assert!(msg.content.contains("$20 of usage")); } #[test] diff --git a/crates/jcode-tui/src/tui/app/tests/onboarding_flow.rs b/crates/jcode-tui/src/tui/app/tests/onboarding_flow.rs index bff4fc3286..62bfbbfa3c 100644 --- a/crates/jcode-tui/src/tui/app/tests/onboarding_flow.rs +++ b/crates/jcode-tui/src/tui/app/tests/onboarding_flow.rs @@ -1547,7 +1547,9 @@ fn import_summary_choose_pill_opens_checkbox_list() { import: Some(review), }; } - // Arrow to the "Import less" pill, then commit it. + // Arrow past the subscription option to the "Import less" pill, then + // commit it. + assert!(app.handle_onboarding_continue_prompt_key(KeyCode::Right)); assert!(app.handle_onboarding_continue_prompt_key(KeyCode::Right)); assert!(app.handle_onboarding_continue_prompt_key(KeyCode::Enter)); // Now in choose mode: the checkbox list with the cursor on row 1 and @@ -1688,7 +1690,9 @@ fn telemetry_pill_opens_settings_page_and_commits_choice() { }; } - // Right twice: Continue -> Import less -> Telemetry settings. + // Right three times: Continue -> Subscription -> Import less -> + // Telemetry settings. + assert!(app.handle_onboarding_continue_prompt_key(KeyCode::Right)); assert!(app.handle_onboarding_continue_prompt_key(KeyCode::Right)); assert!(app.handle_onboarding_continue_prompt_key(KeyCode::Right)); assert!(app.handle_onboarding_continue_prompt_key(KeyCode::Enter)); diff --git a/crates/jcode-tui/src/tui/app/tests/remote_events_reload_01/part_02.rs b/crates/jcode-tui/src/tui/app/tests/remote_events_reload_01/part_02.rs index 30c29cf2a9..f4e5bad646 100644 --- a/crates/jcode-tui/src/tui/app/tests/remote_events_reload_01/part_02.rs +++ b/crates/jcode-tui/src/tui/app/tests/remote_events_reload_01/part_02.rs @@ -158,6 +158,9 @@ fn test_remote_auto_poke_challenges_abrupt_confidence_increase() { delivery_state: Some(crate::todo::DeliveryState::WorkflowValidated), autonomy: Some(crate::todo::Autonomy::NecessaryFollowthrough), iteration_maturity: Some(crate::todo::IterationMaturity::OutcomeReached), + feedback_loop_relevance: Some(crate::todo::FeedbackLoopRelevance::Representative), + feedback_loop_coverage: Some(crate::todo::FeedbackLoopCoverage::MainPaths), + feedback_loop_traceability: Some(crate::todo::FeedbackLoopTraceability::Complete), ..Default::default() }], ) @@ -216,6 +219,9 @@ fn test_remote_auto_poke_completion_below_threshold_tells_model_to_keep_working( delivery_state: Some(crate::todo::DeliveryState::WorkflowValidated), autonomy: Some(crate::todo::Autonomy::NecessaryFollowthrough), iteration_maturity: Some(crate::todo::IterationMaturity::OutcomeReached), + feedback_loop_relevance: Some(crate::todo::FeedbackLoopRelevance::Representative), + feedback_loop_coverage: Some(crate::todo::FeedbackLoopCoverage::MainPaths), + feedback_loop_traceability: Some(crate::todo::FeedbackLoopTraceability::Complete), ..Default::default() }], ) diff --git a/crates/jcode-tui/src/tui/app/tests/remote_events_reload_05.rs b/crates/jcode-tui/src/tui/app/tests/remote_events_reload_05.rs index 603367f163..4ca6685373 100644 --- a/crates/jcode-tui/src/tui/app/tests/remote_events_reload_05.rs +++ b/crates/jcode-tui/src/tui/app/tests/remote_events_reload_05.rs @@ -148,6 +148,9 @@ fn test_reload_preserves_completed_confidence_spike_challenge() { delivery_state: Some(crate::todo::DeliveryState::WorkflowValidated), autonomy: Some(crate::todo::Autonomy::NecessaryFollowthrough), iteration_maturity: Some(crate::todo::IterationMaturity::OutcomeReached), + feedback_loop_relevance: Some(crate::todo::FeedbackLoopRelevance::Representative), + feedback_loop_coverage: Some(crate::todo::FeedbackLoopCoverage::MainPaths), + feedback_loop_traceability: Some(crate::todo::FeedbackLoopTraceability::Complete), ..Default::default() }], ) @@ -293,6 +296,9 @@ fn remote_ownership_gate_reads_the_remote_goal_assessment() { delivery_state: Some(crate::todo::DeliveryState::WorkflowValidated), autonomy: Some(crate::todo::Autonomy::NecessaryFollowthrough), iteration_maturity: Some(crate::todo::IterationMaturity::OutcomeReached), + feedback_loop_relevance: Some(crate::todo::FeedbackLoopRelevance::Representative), + feedback_loop_coverage: Some(crate::todo::FeedbackLoopCoverage::MainPaths), + feedback_loop_traceability: Some(crate::todo::FeedbackLoopTraceability::Complete), ..Default::default() }], ) @@ -484,6 +490,9 @@ fn test_gate_digest_is_delivered_at_turn_end_and_rearms_next_cycle() { delivery_state: Some(crate::todo::DeliveryState::WorkflowValidated), autonomy: Some(crate::todo::Autonomy::NecessaryFollowthrough), iteration_maturity: Some(crate::todo::IterationMaturity::OutcomeReached), + feedback_loop_relevance: Some(crate::todo::FeedbackLoopRelevance::Representative), + feedback_loop_coverage: Some(crate::todo::FeedbackLoopCoverage::MainPaths), + feedback_loop_traceability: Some(crate::todo::FeedbackLoopTraceability::Complete), ..Default::default() }], ) @@ -652,6 +661,9 @@ fn completed_cycle_rearms_auto_poke_only_when_default_on() { delivery_state: Some(crate::todo::DeliveryState::WorkflowValidated), autonomy: Some(crate::todo::Autonomy::NecessaryFollowthrough), iteration_maturity: Some(crate::todo::IterationMaturity::OutcomeReached), + feedback_loop_relevance: Some(crate::todo::FeedbackLoopRelevance::Representative), + feedback_loop_coverage: Some(crate::todo::FeedbackLoopCoverage::MainPaths), + feedback_loop_traceability: Some(crate::todo::FeedbackLoopTraceability::Complete), ..Default::default() }], ) @@ -674,6 +686,9 @@ fn completed_cycle_rearms_auto_poke_only_when_default_on() { delivery_state: Some(crate::todo::DeliveryState::WorkflowValidated), autonomy: Some(crate::todo::Autonomy::NecessaryFollowthrough), iteration_maturity: Some(crate::todo::IterationMaturity::OutcomeReached), + feedback_loop_relevance: Some(crate::todo::FeedbackLoopRelevance::Representative), + feedback_loop_coverage: Some(crate::todo::FeedbackLoopCoverage::MainPaths), + feedback_loop_traceability: Some(crate::todo::FeedbackLoopTraceability::Complete), ..Default::default() }], ) diff --git a/crates/jcode-tui/src/tui/app/tests/state_model_poke_02/part_01.rs b/crates/jcode-tui/src/tui/app/tests/state_model_poke_02/part_01.rs index b3dc13c827..16d175da8a 100644 --- a/crates/jcode-tui/src/tui/app/tests/state_model_poke_02/part_01.rs +++ b/crates/jcode-tui/src/tui/app/tests/state_model_poke_02/part_01.rs @@ -1059,7 +1059,7 @@ fn test_context_command_reports_session_context_snapshot() { assert!(msg.content.contains("Todos")); assert!(msg.content.contains("Side Panel")); assert!(msg.content.contains("Inspect context summary")); - assert!(msg.content.contains("[pending|high|confidence 77%]")); + assert!(msg.content.contains("[pending|high|confidence plausible]")); assert!(msg.content.contains("active skill: debug")); assert!(msg.content.contains("queue mode: on")); }); diff --git a/crates/jcode-tui/src/tui/app/tests/state_model_poke_03.rs b/crates/jcode-tui/src/tui/app/tests/state_model_poke_03.rs index 5933511b7e..4bea57ce6b 100644 --- a/crates/jcode-tui/src/tui/app/tests/state_model_poke_03.rs +++ b/crates/jcode-tui/src/tui/app/tests/state_model_poke_03.rs @@ -2639,6 +2639,9 @@ fn test_finish_turn_challenges_confidence_spike_once() { delivery_state: Some(crate::todo::DeliveryState::WorkflowValidated), autonomy: Some(crate::todo::Autonomy::NecessaryFollowthrough), iteration_maturity: Some(crate::todo::IterationMaturity::OutcomeReached), + feedback_loop_relevance: Some(crate::todo::FeedbackLoopRelevance::Representative), + feedback_loop_coverage: Some(crate::todo::FeedbackLoopCoverage::MainPaths), + feedback_loop_traceability: Some(crate::todo::FeedbackLoopTraceability::Complete), ..Default::default() }], ) From a7e6f81ad2805ba17aee924f55463af33e2177e0 Mon Sep 17 00:00:00 2001 From: jeremy <94247773+1jehuang@users.noreply.github.com> Date: Thu, 6 Aug 2026 03:49:12 -0700 Subject: [PATCH 40/45] chore(ci): sync TUI fixture ratchets --- scripts/code_size_budget.json | 2 +- scripts/test_size_budget.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/code_size_budget.json b/scripts/code_size_budget.json index 03634d6086..5588deaa60 100644 --- a/scripts/code_size_budget.json +++ b/scripts/code_size_budget.json @@ -80,7 +80,7 @@ "crates/jcode-tui/src/tui/app/input.rs": 4022, "crates/jcode-tui/src/tui/app/model_context.rs": 1945, "crates/jcode-tui/src/tui/app/navigation.rs": 1843, - "crates/jcode-tui/src/tui/app/onboarding_flow_control.rs": 1757, + "crates/jcode-tui/src/tui/app/onboarding_flow_control.rs": 1754, "crates/jcode-tui/src/tui/app/remote.rs": 2079, "crates/jcode-tui/src/tui/app/remote/key_handling.rs": 2635, "crates/jcode-tui/src/tui/app/remote/server_events.rs": 2832, diff --git a/scripts/test_size_budget.json b/scripts/test_size_budget.json index 75bb4c1545..c5171b5eaa 100644 --- a/scripts/test_size_budget.json +++ b/scripts/test_size_budget.json @@ -22,7 +22,7 @@ "crates/jcode-tui/src/tui/app/tests.rs": 1750, "crates/jcode-tui/src/tui/app/tests/commands_accounts_01/part_01.rs": 1673, "crates/jcode-tui/src/tui/app/tests/onboarding_eval.rs": 3302, - "crates/jcode-tui/src/tui/app/tests/onboarding_flow.rs": 1813, + "crates/jcode-tui/src/tui/app/tests/onboarding_flow.rs": 1817, "crates/jcode-tui/src/tui/app/tests/remote_events_reload_01/part_01.rs": 1930, "crates/jcode-tui/src/tui/app/tests/remote_events_reload_04.rs": 2415, "crates/jcode-tui/src/tui/app/tests/remote_startup_input_02/part_01.rs": 1998, @@ -32,7 +32,7 @@ "crates/jcode-tui/src/tui/app/tests/scroll_copy_03.rs": 1842, "crates/jcode-tui/src/tui/app/tests/state_model_poke_01/part_01.rs": 1356, "crates/jcode-tui/src/tui/app/tests/state_model_poke_02/part_01.rs": 1376, - "crates/jcode-tui/src/tui/app/tests/state_model_poke_03.rs": 2867, + "crates/jcode-tui/src/tui/app/tests/state_model_poke_03.rs": 2870, "crates/jcode-tui/src/tui/app/tests/swarm_plan_graph_inline.rs": 1979, "crates/jcode-tui/src/tui/info_widget_tests.rs": 1815, "crates/jcode-tui/src/tui/session_picker/loading_tests.rs": 1401, From 893f22a0fc316befe9ace3462b605218cdf0634a Mon Sep 17 00:00:00 2001 From: jeremy <94247773+1jehuang@users.noreply.github.com> Date: Thu, 6 Aug 2026 04:02:33 -0700 Subject: [PATCH 41/45] Default onboarding to Jcode subscription --- crates/jcode-tui/src/tui/app/onboarding_flow.rs | 10 +++++----- crates/jcode-tui/src/tui/app/tests/onboarding_eval.rs | 3 ++- .../jcode-tui/src/tui/app/tests/onboarding_golden.rs | 11 ++++++----- crates/jcode-tui/src/tui/app/tests/onboarding_sim.rs | 6 +++--- crates/jcode-tui/src/tui/ui_onboarding.rs | 9 +-------- 5 files changed, 17 insertions(+), 22 deletions(-) diff --git a/crates/jcode-tui/src/tui/app/onboarding_flow.rs b/crates/jcode-tui/src/tui/app/onboarding_flow.rs index 43d3947505..d6d9a5d59b 100644 --- a/crates/jcode-tui/src/tui/app/onboarding_flow.rs +++ b/crates/jcode-tui/src/tui/app/onboarding_flow.rs @@ -152,9 +152,9 @@ pub(crate) struct ImportReview { /// The actions on the import summary screen, left to right. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub(crate) enum SummaryPill { - /// Import every detected login and move on (default). + /// Import every detected login and move on. Continue, - /// Skip importing and sign in with a Jcode subscription instead. + /// Skip importing and sign in with a Jcode subscription instead (default). Subscription, /// Open the per-login checkbox list to import fewer logins. ImportLess, @@ -188,7 +188,7 @@ impl SummaryPill { impl ImportReview { /// Create a review for the given candidates with every login pre-checked, - /// starting on the summary screen with "Continue" preselected. + /// starting on the summary screen with the Jcode subscription preselected. /// Returns `None` if there are no candidates. pub(crate) fn new( candidates: Vec, @@ -201,9 +201,9 @@ impl ImportReview { candidates, checked, cursor: 0, - continue_focused: true, + continue_focused: false, choosing: false, - summary_pill: SummaryPill::Continue, + summary_pill: SummaryPill::Subscription, telemetry: None, shown_at: Instant::now(), }) diff --git a/crates/jcode-tui/src/tui/app/tests/onboarding_eval.rs b/crates/jcode-tui/src/tui/app/tests/onboarding_eval.rs index 667fddee20..24f52b81bd 100644 --- a/crates/jcode-tui/src/tui/app/tests/onboarding_eval.rs +++ b/crates/jcode-tui/src/tui/app/tests/onboarding_eval.rs @@ -1394,9 +1394,10 @@ fn tier8_metrics() -> Tier8Metrics { let mut app = create_test_app(); app.onboarding_flow = None; app.begin_onboarding_flow_at_login(); - let review = + let mut review = ImportReview::new(vec![ExternalAuthReviewCandidate::fixture("OpenAI/Codex", "Codex auth.json")]) .unwrap(); + review.enter_choose_mode(); if let Some(flow) = app.onboarding_flow.as_mut() { flow.phase = OnboardingPhase::Login { import: Some(review) }; } diff --git a/crates/jcode-tui/src/tui/app/tests/onboarding_golden.rs b/crates/jcode-tui/src/tui/app/tests/onboarding_golden.rs index eff693467c..131107e062 100644 --- a/crates/jcode-tui/src/tui/app/tests/onboarding_golden.rs +++ b/crates/jcode-tui/src/tui/app/tests/onboarding_golden.rs @@ -134,14 +134,15 @@ fn onboarding_golden_walks_every_phase() { // The primary actions explicitly offer import or a Jcode subscription. assert!(text.contains("Import"), "import pill label: {text}"); assert!( - text.contains("Use Jcode subscription"), + text.contains("Jcode subscription (50% off)"), "subscription pill label: {text}" ); assert!(text.contains("Import less"), "import-less pill: {text}"); assert!(text.contains("Telemetry"), "telemetry pill label: {text}"); - assert!( - text.contains("Jcode hosted models are 50% off provider API prices."), - "hosted-model discount: {text}" + assert_eq!( + text.matches("50% off").count(), + 1, + "discount belongs in the subscription pill only: {text}" ); assert!( text.contains('\u{25D6}') && text.contains('\u{25D7}'), @@ -205,7 +206,7 @@ fn onboarding_golden_walks_every_phase() { assert!(text.contains("Cursor"), "single login row: {text}"); assert!(text.contains("Import"), "import pill: {text}"); assert!( - text.contains("Use Jcode subscription"), + text.contains("Jcode subscription (50% off)"), "subscription pill: {text}" ); } diff --git a/crates/jcode-tui/src/tui/app/tests/onboarding_sim.rs b/crates/jcode-tui/src/tui/app/tests/onboarding_sim.rs index 42e88cc74d..d1d49ba73f 100644 --- a/crates/jcode-tui/src/tui/app/tests/onboarding_sim.rs +++ b/crates/jcode-tui/src/tui/app/tests/onboarding_sim.rs @@ -202,13 +202,13 @@ fn onboarding_sim_summary_arrows_preview_all_pills() { }) => review.summary_pill, other => panic!("expected import summary, got {other:?}"), }; - assert_eq!(pill(&app), SummaryPill::Continue); - app.handle_key(KeyCode::Right, KeyModifiers::NONE).unwrap(); assert_eq!(pill(&app), SummaryPill::Subscription); app.handle_key(KeyCode::Right, KeyModifiers::NONE).unwrap(); assert_eq!(pill(&app), SummaryPill::ImportLess); app.handle_key(KeyCode::Right, KeyModifiers::NONE).unwrap(); assert_eq!(pill(&app), SummaryPill::Telemetry); + app.handle_key(KeyCode::Right, KeyModifiers::NONE).unwrap(); + assert_eq!(pill(&app), SummaryPill::Continue); app.handle_key(KeyCode::Left, KeyModifiers::NONE).unwrap(); - assert_eq!(pill(&app), SummaryPill::ImportLess); + assert_eq!(pill(&app), SummaryPill::Telemetry); } diff --git a/crates/jcode-tui/src/tui/ui_onboarding.rs b/crates/jcode-tui/src/tui/ui_onboarding.rs index a10cffe46d..c510b018e8 100644 --- a/crates/jcode-tui/src/tui/ui_onboarding.rs +++ b/crates/jcode-tui/src/tui/ui_onboarding.rs @@ -147,7 +147,7 @@ fn import_summary_pills_line( spans.extend(lozenge_pill_spans("Import", focused == Pill::Continue)); spans.push(Span::raw(" ")); spans.extend(lozenge_pill_spans( - "Use Jcode subscription", + "Jcode subscription (50% off)", focused == Pill::Subscription, )); spans.push(Span::raw(" ")); @@ -536,13 +536,6 @@ fn welcome_body_lines(app: &dyn TuiState) -> Vec> { lines.extend(import_summary_lines(&prompt)); lines.push(Line::from("")); lines.push(import_summary_pills_line(prompt.summary_pill, align)); - lines.push( - Line::from(Span::styled( - "Jcode hosted models are 50% off provider API prices.", - Style::default().fg(dim_color()), - )) - .alignment(align), - ); } Some(prompt) => { // Choose mode: a short "Import:" label, the Continue pill, From b3286d9e8459903a331dccbd4533bfbaf683b075 Mon Sep 17 00:00:00 2001 From: jeremy <94247773+1jehuang@users.noreply.github.com> Date: Thu, 6 Aug 2026 04:31:11 -0700 Subject: [PATCH 42/45] test(security): avoid secret-shaped source literal --- crates/jcode-app-core/src/tool/discover.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/crates/jcode-app-core/src/tool/discover.rs b/crates/jcode-app-core/src/tool/discover.rs index 79b8f8cbb4..d826e16620 100644 --- a/crates/jcode-app-core/src/tool/discover.rs +++ b/crates/jcode-app-core/src/tool/discover.rs @@ -1723,9 +1723,8 @@ mod tests { assert_eq!(normalize_selection_name(None).unwrap(), None); assert!(normalize_selection_name(Some("x")).is_err()); assert!(normalize_selection_name(Some("")).is_err()); - assert!( - normalize_selection_name(Some("ghp_abcdefghijklmnopqrstuvwxyz1234567890")).is_err() - ); + let secret_shaped = format!("{}{}", "gh", "p_abcdefghijklmnopqrstuvwxyz1234567890"); + assert!(normalize_selection_name(Some(&secret_shaped)).is_err()); } #[test] From 8db5c4e8be66fe452b28f3c447f4a70da105d06b Mon Sep 17 00:00:00 2001 From: jeremy <94247773+1jehuang@users.noreply.github.com> Date: Thu, 6 Aug 2026 04:41:58 -0700 Subject: [PATCH 43/45] chore(ci): sync onboarding test ratchet --- scripts/test_size_budget.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/test_size_budget.json b/scripts/test_size_budget.json index c5171b5eaa..fa4c3b16f9 100644 --- a/scripts/test_size_budget.json +++ b/scripts/test_size_budget.json @@ -21,7 +21,7 @@ "crates/jcode-provider-openrouter-runtime/src/openrouter_tests.rs": 3119, "crates/jcode-tui/src/tui/app/tests.rs": 1750, "crates/jcode-tui/src/tui/app/tests/commands_accounts_01/part_01.rs": 1673, - "crates/jcode-tui/src/tui/app/tests/onboarding_eval.rs": 3302, + "crates/jcode-tui/src/tui/app/tests/onboarding_eval.rs": 3303, "crates/jcode-tui/src/tui/app/tests/onboarding_flow.rs": 1817, "crates/jcode-tui/src/tui/app/tests/remote_events_reload_01/part_01.rs": 1930, "crates/jcode-tui/src/tui/app/tests/remote_events_reload_04.rs": 2415, From edb8f64af3b664979c512a6e0fb9ae35be1f1c27 Mon Sep 17 00:00:00 2001 From: jeremy <94247773+1jehuang@users.noreply.github.com> Date: Thu, 6 Aug 2026 05:18:42 -0700 Subject: [PATCH 44/45] test(tui): align onboarding fixtures with subscription default --- .../src/tui/app/tests/onboarding_flow.rs | 25 ++++++++++--------- scripts/test_size_budget.json | 2 +- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/crates/jcode-tui/src/tui/app/tests/onboarding_flow.rs b/crates/jcode-tui/src/tui/app/tests/onboarding_flow.rs index 62bfbbfa3c..27e8fca8e5 100644 --- a/crates/jcode-tui/src/tui/app/tests/onboarding_flow.rs +++ b/crates/jcode-tui/src/tui/app/tests/onboarding_flow.rs @@ -240,11 +240,12 @@ fn import_review_collects_checked_logins() { ExternalAuthReviewCandidate::fixture("Gemini", "Gemini CLI"), ]) .unwrap(); - // The default is the summary screen with Continue preselected. + // The default is the summary screen with Jcode subscription preselected. assert!(!review.choosing); - assert!(review.continue_focused); + assert!(!review.continue_focused); + assert_eq!(review.summary_pill, crate::tui::app::onboarding_flow::SummaryPill::Subscription); assert_eq!(review.total(), 3); - // All pre-checked: the default action imports everything. + // All candidates remain pre-checked when the user chooses an import action. assert_eq!(review.approved_indices(), vec![0, 1, 2]); assert_eq!(review.checked_count(), 3); @@ -1407,13 +1408,15 @@ fn import_summary_defaults_to_continue_and_enter_imports_all() { let mut app = create_test_app(); app.onboarding_flow = None; app.begin_onboarding_flow_at_login(); - let review = ImportReview::new(vec![ + let mut review = ImportReview::new(vec![ ExternalAuthReviewCandidate::fixture("OpenAI/Codex", "Codex auth.json"), ExternalAuthReviewCandidate::fixture("Claude", "Claude Code"), ]) .unwrap(); - // The summary screen is the default and lands on Continue. + // Import tests explicitly select Continue because the product default is + // now Jcode subscription. assert!(!review.choosing); + review.focus_summary_pill(crate::tui::app::onboarding_flow::SummaryPill::Continue); assert!(review.continue_focused); if let Some(flow) = app.onboarding_flow.as_mut() { flow.phase = OnboardingPhase::Login { @@ -1459,11 +1462,12 @@ fn import_continue_reaches_ready_quality_first_openai_model() { runtime.block_on(async { app.onboarding_flow = None; app.begin_onboarding_flow_at_login(); - let review = ImportReview::new(vec![ExternalAuthReviewCandidate::fixture( + let mut review = ImportReview::new(vec![ExternalAuthReviewCandidate::fixture( "OpenAI/Codex", "Codex auth.json", )]) .unwrap(); + review.focus_summary_pill(crate::tui::app::onboarding_flow::SummaryPill::Continue); if let Some(flow) = app.onboarding_flow.as_mut() { flow.phase = OnboardingPhase::Login { import: Some(review), @@ -1547,9 +1551,8 @@ fn import_summary_choose_pill_opens_checkbox_list() { import: Some(review), }; } - // Arrow past the subscription option to the "Import less" pill, then - // commit it. - assert!(app.handle_onboarding_continue_prompt_key(KeyCode::Right)); + // Arrow from the default subscription option to the "Import less" pill, + // then commit it. assert!(app.handle_onboarding_continue_prompt_key(KeyCode::Right)); assert!(app.handle_onboarding_continue_prompt_key(KeyCode::Enter)); // Now in choose mode: the checkbox list with the cursor on row 1 and @@ -1690,9 +1693,7 @@ fn telemetry_pill_opens_settings_page_and_commits_choice() { }; } - // Right three times: Continue -> Subscription -> Import less -> - // Telemetry settings. - assert!(app.handle_onboarding_continue_prompt_key(KeyCode::Right)); + // Right twice: Subscription -> Import less -> Telemetry settings. assert!(app.handle_onboarding_continue_prompt_key(KeyCode::Right)); assert!(app.handle_onboarding_continue_prompt_key(KeyCode::Right)); assert!(app.handle_onboarding_continue_prompt_key(KeyCode::Enter)); diff --git a/scripts/test_size_budget.json b/scripts/test_size_budget.json index fa4c3b16f9..29376e01b6 100644 --- a/scripts/test_size_budget.json +++ b/scripts/test_size_budget.json @@ -22,7 +22,7 @@ "crates/jcode-tui/src/tui/app/tests.rs": 1750, "crates/jcode-tui/src/tui/app/tests/commands_accounts_01/part_01.rs": 1673, "crates/jcode-tui/src/tui/app/tests/onboarding_eval.rs": 3303, - "crates/jcode-tui/src/tui/app/tests/onboarding_flow.rs": 1817, + "crates/jcode-tui/src/tui/app/tests/onboarding_flow.rs": 1818, "crates/jcode-tui/src/tui/app/tests/remote_events_reload_01/part_01.rs": 1930, "crates/jcode-tui/src/tui/app/tests/remote_events_reload_04.rs": 2415, "crates/jcode-tui/src/tui/app/tests/remote_startup_input_02/part_01.rs": 1998, From 09d328b1c636d5fd706bf23050eb795c5270f47a Mon Sep 17 00:00:00 2001 From: jeremy <94247773+1jehuang@users.noreply.github.com> Date: Thu, 6 Aug 2026 16:37:16 -0700 Subject: [PATCH 45/45] chore(release): prepare v0.71.0 --- Cargo.lock | 2 +- Cargo.toml | 2 +- changelog/index.json | 4 ++++ changelog/v0.71.0.json | 20 ++++++++++++++++++++ 4 files changed, 26 insertions(+), 2 deletions(-) create mode 100644 changelog/v0.71.0.json diff --git a/Cargo.lock b/Cargo.lock index ab16783e7d..7bfa396ecc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3311,7 +3311,7 @@ checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" [[package]] name = "jcode" -version = "0.70.1" +version = "0.71.0" dependencies = [ "anyhow", "async-stream", diff --git a/Cargo.toml b/Cargo.toml index 0f414e2893..37f3a23449 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "jcode" -version = "0.70.1" +version = "0.71.0" description = "Possibly the greatest coding agent ever built — blazing-fast TUI, multi-model, swarm coordination, 30+ tools" edition = "2024" autobins = false diff --git a/changelog/index.json b/changelog/index.json index 9e0f90ff61..3822d0d3cd 100644 --- a/changelog/index.json +++ b/changelog/index.json @@ -1,5 +1,9 @@ { "entries": [ + { + "version": "0.71.0", + "date": "2026-08-06" + }, { "version": "0.70.1", "date": "2026-08-06" diff --git a/changelog/v0.71.0.json b/changelog/v0.71.0.json new file mode 100644 index 0000000000..ca9321535a --- /dev/null +++ b/changelog/v0.71.0.json @@ -0,0 +1,20 @@ +{ + "version": "0.71.0", + "date": "2026-08-06", + "title": "Subscription onboarding and broader model support", + "highlights": [ + "Onboarding now defaults to the Jcode subscription, with clearer hosted-model pricing and a direct path to subscription details", + "OpenAI users can choose the full GPT-5.6 family, including Terra, and Meta Model API users can configure Muse and DeepSeek passback models" + ], + "improvements": [ + "Todo quality feedback now distinguishes synthetic validation and tracks requirement-to-check traceability", + "Cargo commands report action durations to make slow build and test steps easier to identify" + ], + "fixes": [ + "Scheduled turns no longer leak into user prompt history", + "Empty transcript checkpoints are prevented so session history remains usable", + "Terminal launches fall back cleanly when spawn hooks reject a launch", + "The TypeScript SDK waits for daemon registration before closing launched sessions", + "Permanent memory sidecar failures are distinguished from transient failures and use the current Claude model" + ] +}