Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
6bceece
[codex/vc-ship] feat(core): lane_truth resolver — one truth for keys,…
div0-space Jul 11, 2026
4d79816
[codex/vc-ship] feat(settings): editable LLM lanes in Engine panel
div0-space Jul 11, 2026
a2d388c
[codex/vc-ship] feat(config): promote LLM_ASSISTIVE_PROVIDER to setti…
div0-space Jul 11, 2026
65e06f1
[claude/workflow] feat(core): assistive lane snapshot + availability …
div0-space Jul 11, 2026
45f6374
[claude/workflow] fix(agent): build providers from lane truth — fresh…
div0-space Jul 11, 2026
dfb5357
[claude/workflow] fix(chat): actionable unavailable reason + real lan…
div0-space Jul 11, 2026
d78a927
[codex/vc-ship] feat(bridge): probe endpoint truth + live readiness r…
div0-space Jul 11, 2026
472f6bc
[claude/interactive] fix(brand): purge camelCase CodeScribe regressions
div0-space Jul 11, 2026
7bbf228
[codex/vc-ship] fix(gates): executable agent-gate verifier + honest p…
div0-space Jul 11, 2026
88e5e82
[claude/vc-ship] style(docs): prettier 2.7.1 baseline for tracked docs
div0-space Jul 11, 2026
75b6ecb
[codex/vc-ship] fix(settings): per-lane runtime truth rows + reset de…
div0-space Jul 11, 2026
d15c105
[claude/interactive] feat(build): stamp app with version, build numbe…
div0-space Jul 11, 2026
b298ac9
[claude/workflow] feat(auth): operator-configurable ChatGPT OAuth cli…
div0-space Jul 12, 2026
e5f9aa2
[claude/workflow] feat(agent): assistive lane runs on ChatGPT account…
div0-space Jul 12, 2026
490ceae
[claude/workflow] feat(ui): wire the Sign in with ChatGPT button end-…
div0-space Jul 12, 2026
3a79051
[codex/vc-polarize] refactor(llm): lane_truth as sole lane resolution…
div0-space Jul 12, 2026
845cec0
[codex/vc-workflow] fix(overlay): append utterances + phase disciplin…
div0-space Jul 12, 2026
8add684
[junie/vc-polarize] refactor(settings): one lane-parametrized LLM tru…
div0-space Jul 12, 2026
dac500d
[grok/vc-workflow] feat(quality): P0-D overlay edit capture → quality…
div0-space Jul 12, 2026
cc2ebdf
[junie/vc-workflow] fix(delivery): unify live sink parity
div0-space Jul 12, 2026
7d9f6e8
[codex/vc-workflow] fix(delivery): patch late corrections idempotently
div0-space Jul 12, 2026
37f137e
[junie/vc-workflow] fix(pipeline): honor raw lexicon and settings for…
div0-space Jul 12, 2026
8190037
[junie/vc-polarize] refactor(settings): L2+L3 — one discovery path, o…
div0-space Jul 12, 2026
509ee94
[claude/vc-workflow] docs(auth): SAFETY comments on every env mutatio…
div0-space Jul 12, 2026
21dbc88
[claude/vc-workflow] docs(safety): SAFETY on EnvVarGuard + stop-laten…
div0-space Jul 12, 2026
7afb253
[claude/vc-workflow] fix(overlay): quality capture off the main actor…
div0-space Jul 12, 2026
1764059
[codex/vc-workflow] fix(config): refresh API keys without env mutation
div0-space Jul 11, 2026
878ff00
[claude/interactive] fix(build): pin ONLY_ACTIVE_ARCH=YES in app pipe…
div0-space Jul 12, 2026
74ec859
[claude/interactive] feat(overlay): anchored placement + fade on agen…
div0-space Jul 12, 2026
f48afad
[grok/vc-marbles] fix(P1-02/D-02/E-01/P2-05/D-05/P2-07/P2-08/P2-09/P3…
div0-space Jul 12, 2026
33bd1cb
[grok/vc-marbles] fix(P1-02/D-02/D-05/P2-05/P3): quality action-in-me…
div0-space Jul 12, 2026
6e1189a
[grok/vc-marbles] fix(D-05/P2-07/P2-08/P2-09): wire sttRawText for qu…
div0-space Jul 12, 2026
5fff2b3
[grok/vc-marbles] fix(P1-02/D-02/P2-07/P3-03): deepen quality loop te…
div0-space Jul 12, 2026
e1f32d8
[grok/vc-marbles] fix(P1-02/D-02/P2-07/P3/E-01): deepen quality isola…
div0-space Jul 12, 2026
8e5d31d
[claude/interactive] fix(streaming): correction splices into accumula…
div0-space Jul 12, 2026
f815f4c
[claude/interactive] fix(history): collapse same-second save families…
div0-space Jul 12, 2026
1da9e16
[claude/interactive] fix(overlay): landscape default restored + assis…
div0-space Jul 12, 2026
5653511
[codex/vc-workflow] fix(pr): close merge-readiness review gaps
div0-space Jul 12, 2026
769aa03
[claude/vc-workflow] fix(auth): login-server cancel can no longer be …
div0-space Jul 12, 2026
80a8d36
[junie/vc-hydrate] docs(release): lane-truth README + changelog + lan…
div0-space Jul 12, 2026
7c14bdf
Revert "[junie/vc-hydrate] docs(release): lane-truth README + changel…
div0-space Jul 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,16 @@
**/.pytest_cache/**
**/.venv/**
**/.DS_Store
**/AGENTS.md
**/CLAUDE.md
**/*codex*.md
**/claude_memory_*.md
**/Codescribe_memory_*.md
**/logs/**
**/node_modules/**
**/build/**
**/.build/**
**/DerivedData/**
**/target/**
**/target-noembed/**
**/dist/**
Expand All @@ -38,3 +45,4 @@
**/*.mp3
**/*.m4a
**/*.zip
**/*.xcassets/**
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ release-codescribe-embedded: release-codescribe
# Full verified pipeline: cargo (ffi dylib) → uniffi-bindgen → xcodegen → xcodebuild.
# `app-bindings` stops after xcodegen (no Xcode needed) for fast Rust-side iteration.
app:
@echo "Building CodeScribe.app (SwiftUI, PROFILE=$(PROFILE))..."
@echo "Building Codescribe.app (SwiftUI, PROFILE=$(PROFILE))..."
@./scripts/build-app.sh $(PROFILE)

app-bindings:
Expand Down
37 changes: 12 additions & 25 deletions app/agent/anthropic_provider.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,14 +42,10 @@ use codescribe_core::agent::{
};
use codescribe_core::llm::provider::{ProviderKind, capability_policy};

const DEFAULT_ANTHROPIC_ENDPOINT: &str = "https://api.anthropic.com/v1/messages";
const ANTHROPIC_VERSION: &str = "2023-06-01";
/// Anthropic requires `max_tokens` on every request; used only when the caller
/// (assistive lane) leaves `options.max_tokens` unset.
const DEFAULT_MAX_TOKENS: u32 = 8192;
/// Safe Claude default so the agent path never falls into an empty model when
/// the assistive-lane model env is unset (Settings normally supplies one).
const DEFAULT_ANTHROPIC_MODEL: &str = "claude-opus-4-8";

const DEFAULT_INITIAL_RESPONSE_TIMEOUT_MS: u64 = 90_000;
const DEFAULT_INTER_CHUNK_TIMEOUT_MS: u64 = 90_000;
Expand All @@ -69,20 +65,20 @@ pub struct AnthropicProvider {
}

impl AnthropicProvider {
pub fn from_env() -> Result<Self> {
let api_key = get_env_non_empty("LLM_ANTHROPIC_API_KEY", "Anthropic API key (assistive)")?;
let endpoint = std::env::var("LLM_ANTHROPIC_ENDPOINT")
.ok()
.map(|value| value.trim().to_string())
.filter(|value| !value.is_empty())
.unwrap_or_else(|| DEFAULT_ANTHROPIC_ENDPOINT.to_string());
/// Build from the resolved assistive lane (fresh settings → env →
/// Keychain). Anthropic always authenticates, so a missing key is a
/// readable error naming the exact account — the availability gate
/// reports the same reason before a send is ever attempted.
pub fn from_lane(
lane: codescribe_core::llm::lane_truth::AssistiveLaneSnapshot,
) -> Result<Self> {
let api_key = lane
.api_key
.context("Anthropic API key (assistive) is required. Set LLM_ANTHROPIC_API_KEY.")?;
let endpoint = lane.endpoint;
// Model comes from the shared assistive-lane setting; Settings supplies a
// Claude model when the assistive provider is Anthropic.
let default_model = std::env::var("LLM_ASSISTIVE_MODEL")
.ok()
.map(|value| value.trim().to_string())
.filter(|value| !value.is_empty())
.unwrap_or_else(|| DEFAULT_ANTHROPIC_MODEL.to_string());
let default_model = lane.model;

let initial_response_timeout = Duration::from_millis(parse_env_u64(
"CODESCRIBE_AI_ATTEMPT_TIMEOUT_MS",
Expand Down Expand Up @@ -805,15 +801,6 @@ fn validate_anthropic_endpoint(endpoint: &str) -> Result<reqwest::Url> {
Ok(url)
}

fn get_env_non_empty(key: &str, label: &str) -> Result<String> {
let value = std::env::var(key).with_context(|| format!("{label} is required. Set {key}."))?;
let trimmed = value.trim();
if trimmed.is_empty() {
anyhow::bail!("{label} is required. Set {key}.");
}
Ok(trimmed.to_string())
}

fn parse_env_u64(key: &str, default: u64) -> u64 {
std::env::var(key)
.ok()
Expand Down
34 changes: 21 additions & 13 deletions app/agent/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
use anyhow::Result;
use codescribe_core::agent::AgentProvider;
use codescribe_core::llm::provider::{LlmMode, ProviderKind, resolve_provider};
use codescribe_core::config::Config;
use codescribe_core::llm::lane_truth;
use codescribe_core::llm::provider::ProviderKind;

pub mod anthropic_provider;
pub mod openai_provider;
Expand All @@ -10,18 +12,24 @@ pub mod tools;
pub use anthropic_provider::AnthropicProvider;
pub use openai_provider::OpenAiProvider;

pub fn create_openai_provider_from_env() -> Result<OpenAiProvider> {
OpenAiProvider::from_env()
}

/// Build the agent (assistive-lane) provider from the configured provider
/// identity (`LLM_ASSISTIVE_PROVIDER`, resolved by
/// [`resolve_provider`]). A missing key for the SELECTED provider surfaces its
/// own clear error (`from_env`) rather than silently falling back to the other
/// provider — misconfiguration must never route to an unintended vendor.
/// Build the agent (assistive-lane) provider from the lane-truth snapshot
/// (fresh settings → env → Keychain), so a Settings save is honored on the
/// very next send — no restart, no stale bootstrap env. A lane that cannot be
/// reached fails with the same actionable reason
/// [`assistive_unavailable_reason`] reports, never a generic key demand, and
/// never a silent fallback to an unintended vendor.
pub fn create_default_provider() -> Result<Box<dyn AgentProvider>> {
match resolve_provider(LlmMode::Assistive) {
ProviderKind::OpenAiResponses => Ok(Box::new(OpenAiProvider::from_env()?)),
ProviderKind::AnthropicMessages => Ok(Box::new(AnthropicProvider::from_env()?)),
let config = Config::load();
let lane = lane_truth::assistive_availability(&config).map_err(anyhow::Error::msg)?;
match lane.provider {
ProviderKind::OpenAiResponses => Ok(Box::new(OpenAiProvider::from_lane(lane)?)),
ProviderKind::AnthropicMessages => Ok(Box::new(AnthropicProvider::from_lane(lane)?)),
}
}

/// User-facing reason the assistive lane cannot reach a model right now
/// (`None` when a send can proceed). Kept beside [`create_default_provider`]
/// so the availability gate and provider construction can never drift.
pub fn assistive_unavailable_reason() -> Option<String> {
lane_truth::assistive_availability(&Config::load()).err()
}
68 changes: 52 additions & 16 deletions app/agent/openai_provider.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,11 @@ use codescribe_core::agent::{
AgentEvent, AgentProvider, ContentBlock, ImageAsset, Message, Role, StreamOptions,
ToolDefinition,
};
use codescribe_core::llm::account_auth;
use codescribe_core::llm::lane_truth::AssistiveLaneSnapshot;
use codescribe_core::llm::provider::ProviderKind;
use codescribe_core::llm::responses_streaming_manager::{
ResponsesStreamingManager, StreamCallbacks,
AuthHeaderMode, ResponsesStreamingManager, StreamCallbacks,
};

const DEFAULT_INITIAL_RESPONSE_TIMEOUT_MS: u64 = 90_000;
Expand Down Expand Up @@ -52,13 +55,27 @@ pub struct OpenAiProvider {
previous_response_id: Arc<Mutex<Option<String>>>,
initial_response_timeout: Duration,
inter_chunk_timeout: Duration,
/// Lane resolved to "Sign in with ChatGPT" account auth (no API key, official
/// endpoint, stored tokens). Each request fetches a FRESH access token via
/// `account_auth` so the auto-refresh path keeps long sessions alive.
use_account_auth: bool,
}

impl OpenAiProvider {
pub fn from_env() -> Result<Self> {
let endpoint = get_env_non_empty("LLM_ASSISTIVE_ENDPOINT", "LLM endpoint (assistive)")?;
let default_model = get_env_non_empty("LLM_ASSISTIVE_MODEL", "LLM model (assistive)")?;
let api_key = get_env_non_empty("LLM_ASSISTIVE_API_KEY", "OpenAI API key (assistive)")?;
/// Build from the resolved assistive lane (fresh settings → env →
/// Keychain) instead of the frozen bootstrap process env. `api_key: None`
/// becomes an empty key, which the streaming manager translates into a
/// clean unauthenticated request — key-optional local endpoints are a
/// first-class configuration, not an error.
pub fn from_lane(lane: AssistiveLaneSnapshot) -> Result<Self> {
let AssistiveLaneSnapshot {
endpoint,
model: default_model,
api_key,
account_auth: use_account_auth,
provider: _,
} = lane;
Comment thread
div0-space marked this conversation as resolved.
let api_key = api_key.unwrap_or_default();

let use_previous_response_id =
parse_env_bool("CODESCRIBE_AGENT_USE_PREVIOUS_RESPONSE_ID", true);
Expand Down Expand Up @@ -93,6 +110,7 @@ impl OpenAiProvider {
previous_response_id: Arc::new(Mutex::new(None)),
initial_response_timeout,
inter_chunk_timeout,
use_account_auth,
})
}
}
Expand Down Expand Up @@ -152,17 +170,39 @@ impl AgentProvider for OpenAiProvider {
stream: true,
};

// Account-auth lanes fetch a fresh access token per request (60s-skew
// auto-refresh) — never a token frozen at provider construction. The
// manager formats the `Bearer` header itself, so this is the raw token.
let account_token = if self.use_account_auth {
Some(
account_auth::access_token(ProviderKind::OpenAiResponses)
.await
.map_err(|error| {
anyhow::anyhow!("ChatGPT account authentication failed: {error}")
})?,
)
} else {
None
};
let auth_secret = account_token.as_deref().unwrap_or(&self.api_key);

let auth_header_mode = if self.use_account_auth {
AuthHeaderMode::BearerOnly
} else {
AuthHeaderMode::BearerAndApiKey
};
let manager = ResponsesStreamingManager::new(
&self.client,
&self.endpoint,
Comment thread
div0-space marked this conversation as resolved.
&self.api_key,
auth_secret,
StreamCallbacks {
assistant: None,
reasoning: None,
},
self.initial_response_timeout,
self.inter_chunk_timeout,
);
)
.with_auth_header_mode(auth_header_mode);

let provider_rx = manager.stream_agent(&request).await?;

Expand Down Expand Up @@ -557,15 +597,6 @@ fn to_data_uri(data: &[u8], media_type: &str) -> String {
format!("data:{media_type};base64,{}", BASE64.encode(data))
}

fn get_env_non_empty(key: &str, label: &str) -> Result<String> {
let value = env::var(key).with_context(|| format!("{label} is required. Set {key}."))?;
let trimmed = value.trim();
if trimmed.is_empty() {
anyhow::bail!("{label} is required. Set {key}.");
}
Ok(trimmed.to_string())
}

fn parse_env_u64(key: &str, default: u64) -> u64 {
env::var(key)
.ok()
Expand Down Expand Up @@ -848,6 +879,7 @@ mod tests {
previous_response_id: Arc::new(Mutex::new(None)),
initial_response_timeout: Duration::from_secs(1),
inter_chunk_timeout: Duration::from_secs(1),
use_account_auth: false,
};
let messages = vec![Message::new(
Role::User,
Expand Down Expand Up @@ -890,6 +922,7 @@ mod tests {
previous_response_id: Arc::clone(&stored_chain),
initial_response_timeout: Duration::from_secs(1),
inter_chunk_timeout: Duration::from_secs(1),
use_account_auth: false,
};

// Pre-condition: stored chain holds prior failed attempt's response id.
Expand Down Expand Up @@ -923,6 +956,7 @@ mod tests {
previous_response_id: Arc::clone(&stored_chain),
initial_response_timeout: Duration::from_secs(1),
inter_chunk_timeout: Duration::from_secs(1),
use_account_auth: false,
};

let options = StreamOptions::default();
Expand Down Expand Up @@ -1001,6 +1035,7 @@ mod tests {
previous_response_id: Arc::clone(&stored_chain),
initial_response_timeout: Duration::from_secs(1),
inter_chunk_timeout: Duration::from_secs(1),
use_account_auth: false,
};
let reset_options = StreamOptions {
reset_chain: true,
Expand Down Expand Up @@ -1177,6 +1212,7 @@ mod tests {
previous_response_id: Arc::clone(&stored_chain),
initial_response_timeout: Duration::from_secs(2),
inter_chunk_timeout: Duration::from_secs(2),
use_account_auth: false,
};
let messages = vec![Message::new(
Role::User,
Expand Down
Loading
Loading