diff --git a/docs/architecture/core-decomposition.md b/docs/architecture/core-decomposition.md index c73507a13..fe484b08e 100644 --- a/docs/architecture/core-decomposition.md +++ b/docs/architecture/core-decomposition.md @@ -257,8 +257,9 @@ owner 边界,否则不要把一个 feature group 继续拆成更小的 crate - 当前 `product-domains` runtime port/facade closure 只迁移 port-backed owner orchestration:MiniApp 的 deps/restart/recompile/sync/rollback 状态持久化可经 storage facade 执行,function-agent commit / work-state facade 可基于 Git/AI port - 组装结果。Git commit-message 产品路径可通过 core-owned Git/AI adapter 接入该 facade; - Startchat work-state 仍需先保留 git-state、git-diff fallback 与 time-info 等价性后再接线。 + 组装结果。Git commit-message 与 Startchat work-state 产品路径可通过 core-owned Git/AI + adapter 接入该 facade;Startchat 接线必须保留 legacy git-state、git-diff fallback、 + `analyze_git=false` time-info 与 `analyzed_at` 时序。 core 仍持有 MiniApp filesystem IO、compiler 调度、worker process、host dispatch、 built-in seed/update,以及 function-agent Git/AI service adapter、prompt template、 JSON extraction 和 error mapping。 diff --git a/docs/plans/core-decomposition-plan.md b/docs/plans/core-decomposition-plan.md index 5fe3ac3ce..0bd93a643 100644 --- a/docs/plans/core-decomposition-plan.md +++ b/docs/plans/core-decomposition-plan.md @@ -1128,7 +1128,7 @@ product-full = ["miniapp", "function-agents"] - 2026-05-18 update: MiniApp draft manifest/response DTO, draft/customization storage path helpers, import layout / fallback payload contracts, manager lifecycle state-transition helpers, runtime executable search-plan helpers, customization draft-apply metadata policy, and built-in update/decline metadata decisions have been moved to `bitfun-product-domains::miniapp`; core continues to own draft/import filesystem IO, compile orchestration, built-in asset seeding/source-hash lookup, host dispatch execution, `PathManager` integration, worker process execution, and compatibility facades. The current PR also records core-owned MiniApp import / sync / recompile / rollback / dependency-state behavior as migration-before tests, including the existing `sync_from_fs` snapshot boundary. - 已迁移到 `bitfun-product-domains::function_agents`:公共 `common` 类型、git/startchat function-agent 的纯 DTO 类型、git function-agent 的纯路径 / 变更分类 / commit summary / message assembly / prompt format / commit type parser / AI response parsing policy、startchat prompt / action / AI response parsing policy / git porcelain / diff combine / time-of-day helper、Git/AI port contract,以及只读本地文件的 project context analyzer;core-owned Git snapshot adapter 已由等价测试覆盖,AI client、Git service、prompt template、AI request、JSON extraction、错误映射与分析运行逻辑仍留在 core。 - 2026-05-18 update: Git function-agent diff truncation and commit prompt preparation are now owner-crate helpers used by core; AI client calls, prompt template ownership, JSON extraction, error mapping, and runtime analysis execution remain core-owned. The current PR adds focused core snapshots for staged-only Git commit diff collection and AI response JSON extraction / error mapping before any Git/AI runtime migration. -- 2026-05-19 update: `bitfun-product-domains` now owns port-backed MiniApp runtime-state and function-agent runtime facades. Core delegates only MiniApp storage-backed lifecycle persistence through the MiniApp facade; compilation, source reads, storage IO adapter, worker process execution, host dispatch, built-in seeding, prompt templates, JSON extraction, and concrete error mapping remain core-owned. The Git commit-message product path now routes through the function-agent facade using core-owned Git/AI adapters; Startchat work-state remains on the existing core path until its git-state, git-diff fallback, and time-info behavior are equivalence-locked for wiring. +- 2026-05-19 update: `bitfun-product-domains` now owns port-backed MiniApp runtime-state and function-agent runtime facades. Core delegates only MiniApp storage-backed lifecycle persistence through the MiniApp facade; compilation, source reads, storage IO adapter, worker process execution, host dispatch, built-in seeding, prompt templates, JSON extraction, and concrete error mapping remain core-owned. The Git commit-message and Startchat work-state product paths now route through the function-agent facade using core-owned Git/AI adapters; Startchat wiring is guarded by focused tests for legacy git-state, no-HEAD git-diff fallback, and `analyze_git=false` time-info, while core keeps the previous post-analysis `analyzed_at` assignment. - boundary check 已补充 product-domain owner anchor:`MiniAppStoragePort` / `MiniAppRuntimePort` 的 core adapter、MiniApp host/customization 纯 contract、MiniApp manager preflight tests、function-agent Git adapter 与 AI response parsing helper 必须存在,防止把 port contract 或 pure parser 误读成 storage IO、worker process、host dispatch、customization draft runtime、Git/AI service runtime 已完成迁移。 - miniapp runtime/storage/manager/host dispatch/exporter/builtin 与 function-agent 运行逻辑继续迁移前,需要先确认 agent/tool/provider port 和 Git/AI service 边界。 @@ -1741,7 +1741,7 @@ git diff -- package.json scripts/dev.cjs scripts/desktop-tauri-build.mjs scripts 15. 已完成:agent tools + `tool-packs` owner 化低风险闭环;tool contract / DTO、runtime restriction、path resolution、portable context facts/provider、generic registry / static provider installation / dynamic provider container 已归属 `bitfun-agent-tools`,`tool-packs` 只提供计划内 feature-group scaffold,core 保留 core-owned product provider groups、snapshot decorator、`ToolUseContext` 和 concrete tool implementation,后续外移需单独 service port/provider 设计。 16. 已完成:关键语义回归 baseline,不移动 runtime owner。覆盖 MCP config failure / catalog invalidation / 既有 list-changed helper / dynamic manifest、tool manifest / `GetToolSpec`、product-domains adapter equivalence、remote workspace search fallback 的 focused tests 或 snapshots。 17. 已完成:remote-connect runtime 当前批次收口。已基于当前 port baseline 记录 remote command/response、remote model catalog、poll response、model catalog delta、session restore、active turn、cancel、image context、tracker event、queue/event fanout 的输入输出和验证命令;tracker state / registry lifecycle、legacy image context fallback / preference、restore target decision、cancel decision 与 remote file transfer size/chunk/name policy 已迁入 `bitfun-services-integrations`。dispatcher / product execution、`ImageContextData` adapter、file IO/path resolution、terminal pre-warm 与 workspace/session restore 执行显式保留在 core-owned runtime;后续只有在另起 port/provider 设计且 focused regression 继续通过时才允许继续移动这些 runtime owner,不能把 generic attachment guard 当作已接入多模态行为。 -18. 当前阶段:`product-domains` runtime port/facade closure。已迁入 MiniApp storage-backed runtime-state facade 与 function-agent Git/AI port-backed runtime facade,并补充 focused contract tests;core 只对 MiniApp deps/restart/recompile/sync/rollback 的状态持久化委托 facade,仍保留 `PathManager` 注入、filesystem IO、worker process execution、host dispatch 执行、built-in asset seeding/source-hash lookup、prompt template、JSON extraction 和 error mapping adapter。Git commit-message 产品路径已通过 core-owned Git/AI adapter 接入 function-agent facade;Startchat work-state 仍保留在旧 core 路径,后续必须先等价锁定 git-state、git-diff fallback 与 time-info 行为再接线。 +18. 当前阶段:`product-domains` runtime port/facade closure。已迁入 MiniApp storage-backed runtime-state facade 与 function-agent Git/AI port-backed runtime facade,并补充 focused contract tests;core 只对 MiniApp deps/restart/recompile/sync/rollback 的状态持久化委托 facade,仍保留 `PathManager` 注入、filesystem IO、worker process execution、host dispatch 执行、built-in asset seeding/source-hash lookup、prompt template、JSON extraction 和 error mapping adapter。Git commit-message 与 Startchat work-state 产品路径已通过 core-owned Git/AI adapter 接入 function-agent facade;Startchat 接线已用 no-HEAD diff fallback、非 Git 目录空状态和 `analyze_git=false` time-info 保护旧行为,`analyzed_at` 仍由 core 在 AI 分析完成后赋值。 19. 后续独立评估:`bitfun-core default = []`、per-product feature set、依赖版本收敛或构建收益优化;任何收益声明都需要记录 `cargo check -p bitfun-core`、workspace check 和目标 crate check 的前后数据。 冗余清理 PR 不进入上述主线序号。只有在满足 `0A.6` 的绝对等价要求时,才可以插入到相邻里程碑之间,并且不得与主线拆分 PR 混合。 diff --git a/scripts/check-core-boundaries.mjs b/scripts/check-core-boundaries.mjs index 53efdc7ed..c9a796b6c 100644 --- a/scripts/check-core-boundaries.mjs +++ b/scripts/check-core-boundaries.mjs @@ -519,6 +519,21 @@ const forbiddenContentRules = [ }, ], }, + { + path: 'src/crates/core/src/function_agents/startchat-func-agent/work_state_analyzer.rs', + patterns: [ + { + regex: /\bAIWorkStateService::new_with_agent_config\b/, + message: + 'Startchat work-state analyzer must use CoreFunctionAgentAiAdapter through FunctionAgentRuntimeFacade', + }, + { + regex: /\bcreate_command\("git"\)/, + message: + 'Startchat work-state analyzer must use CoreFunctionAgentGitAdapter through FunctionAgentRuntimeFacade', + }, + ], + }, { path: 'src/crates/core/src/service/mcp/server/config.rs', patterns: [ @@ -2258,6 +2273,25 @@ const requiredContentRules = [ }, ], }, + { + path: 'src/crates/core/src/function_agents/startchat-func-agent/work_state_analyzer.rs', + reason: + 'Startchat work-state analysis must route through the product-domain runtime facade while core keeps concrete adapters', + patterns: [ + { + regex: /\bFunctionAgentRuntimeFacade\b/, + message: 'missing product-domain function-agent runtime facade routing', + }, + { + regex: /\bCoreFunctionAgentGitAdapter\b/, + message: 'missing core-owned Git adapter wiring', + }, + { + regex: /\bCoreFunctionAgentAiAdapter\b/, + message: 'missing core-owned AI adapter wiring', + }, + ], + }, { path: 'src/crates/product-domains/src/function_agents/ports.rs', reason: @@ -2382,6 +2416,10 @@ const requiredContentRules = [ regex: /\bgit_adapter_commit_snapshot_keeps_staged_diff_and_unstaged_count_separate\b/, message: 'missing function-agent Git snapshot boundary regression test', }, + { + regex: /\bgit_adapter_startchat_snapshot_preserves_git_state_when_diff_has_no_head\b/, + message: 'missing Startchat Git diff fallback regression test', + }, ], }, ]; diff --git a/src/crates/core/AGENTS.md b/src/crates/core/AGENTS.md index a70eb12e3..6ed1c7f73 100644 --- a/src/crates/core/AGENTS.md +++ b/src/crates/core/AGENTS.md @@ -52,10 +52,10 @@ SessionManager → Session → DialogTurn → ModelRound - When touching session/token usage paths, keep `cached_content_token_count` as cache reads/hits and `cache_creation_token_count` as a separate provider fact. -- Function-agent commit-message orchestration may route through - `bitfun-product-domains`; keep Git/AI service adapters, prompt templates, - JSON extraction, and error mapping core-owned until a reviewed migration - proves equivalence. +- Function-agent commit-message and Startchat work-state orchestration may + route through `bitfun-product-domains`; keep Git/AI service adapters, prompt + templates, JSON extraction, and error mapping core-owned until a reviewed + migration proves equivalence. - Do not add new cross-layer references from `service` to `agentic` without a small port/interface boundary. - Do not move platform-specific logic, build-script behavior, or product diff --git a/src/crates/core/src/function_agents/port_adapters.rs b/src/crates/core/src/function_agents/port_adapters.rs index 45f533159..2dc203a99 100644 --- a/src/crates/core/src/function_agents/port_adapters.rs +++ b/src/crates/core/src/function_agents/port_adapters.rs @@ -77,12 +77,12 @@ impl CoreFunctionAgentGitAdapter { } async fn build_startchat_git_snapshot(repo_path: PathBuf) -> AgentResult { - let current_branch = git_stdout(&repo_path, &["branch", "--show-current"])? + let current_branch = git_stdout_lenient(&repo_path, &["branch", "--show-current"])? .trim() .to_string(); - let status_porcelain = git_stdout(&repo_path, &["status", "--porcelain"])?; - let unstaged_diff = git_stdout(&repo_path, &["diff", "HEAD"])?; - let staged_diff = git_stdout(&repo_path, &["diff", "--cached"])?; + let status_porcelain = git_stdout_lenient(&repo_path, &["status", "--porcelain"])?; + let unstaged_diff = git_stdout_lenient(&repo_path, &["diff", "HEAD"])?; + let staged_diff = git_stdout_lenient(&repo_path, &["diff", "--cached"])?; let unpushed_commits = git_unpushed_commits(&repo_path); let ahead_behind = git_ahead_behind(&repo_path); let last_commit_timestamp = git_last_commit_timestamp(&repo_path); @@ -155,27 +155,13 @@ impl FunctionAgentAiPort for CoreFunctionAgentAiAdapter { } } -fn git_stdout(repo_path: &Path, args: &[&str]) -> AgentResult { +fn git_stdout_lenient(repo_path: &Path, args: &[&str]) -> AgentResult { let output = crate::util::process_manager::create_command("git") .args(args) .current_dir(repo_path) .output() .map_err(|e| AgentError::git_error(format!("Failed to run git {:?}: {}", args, e)))?; - if !output.status.success() { - let stderr = String::from_utf8_lossy(&output.stderr); - let stdout = String::from_utf8_lossy(&output.stdout); - let detail = if !stderr.trim().is_empty() { - stderr.trim() - } else { - stdout.trim() - }; - return Err(AgentError::git_error(format!( - "git {:?} failed with status {}: {}", - args, output.status, detail - ))); - } - Ok(String::from_utf8_lossy(&output.stdout).to_string()) } @@ -357,15 +343,43 @@ mod tests { } #[tokio::test] - async fn git_adapter_rejects_startchat_snapshot_when_git_command_fails() { + async fn git_adapter_startchat_snapshot_preserves_git_state_when_diff_has_no_head() { + let repo = TestTempDir::new("startchat-no-head-diff"); + init_git_repo(repo.path()); + fs::write(repo.path().join("new.txt"), "new\n").unwrap(); + + let adapter = CoreFunctionAgentGitAdapter::default(); + let snapshot = adapter + .startchat_git_snapshot(repo.path().to_path_buf()) + .await + .unwrap(); + + assert_eq!(snapshot.current_branch, "main"); + assert!(snapshot.status_porcelain.contains("?? new.txt")); + assert!(snapshot.unstaged_diff.is_empty()); + assert!(snapshot.staged_diff.is_empty()); + assert_eq!(snapshot.unpushed_commits, 0); + assert!(snapshot.ahead_behind.is_none()); + assert!(snapshot.last_commit_timestamp.is_none()); + } + + #[tokio::test] + async fn git_adapter_startchat_snapshot_matches_legacy_empty_state_when_not_git_repo() { let repo = TestTempDir::new("not-a-git-repo"); let adapter = CoreFunctionAgentGitAdapter::default(); - let result = adapter + let snapshot = adapter .startchat_git_snapshot(repo.path().to_path_buf()) - .await; + .await + .unwrap(); - assert!(result.is_err()); + assert!(snapshot.current_branch.is_empty()); + assert!(snapshot.status_porcelain.is_empty()); + assert!(snapshot.unstaged_diff.is_empty()); + assert!(snapshot.staged_diff.is_empty()); + assert_eq!(snapshot.unpushed_commits, 0); + assert!(snapshot.ahead_behind.is_none()); + assert!(snapshot.last_commit_timestamp.is_none()); } fn init_git_repo(repo: &std::path::Path) { diff --git a/src/crates/core/src/function_agents/startchat-func-agent/work_state_analyzer.rs b/src/crates/core/src/function_agents/startchat-func-agent/work_state_analyzer.rs index 955e74ed0..ddbeb3dd3 100644 --- a/src/crates/core/src/function_agents/startchat-func-agent/work_state_analyzer.rs +++ b/src/crates/core/src/function_agents/startchat-func-agent/work_state_analyzer.rs @@ -1,13 +1,17 @@ use super::types::*; -use crate::function_agents::common::{AgentError, AgentResult}; +use crate::function_agents::common::AgentResult; +use crate::function_agents::port_adapters::{ + CoreFunctionAgentAiAdapter, CoreFunctionAgentGitAdapter, +}; use crate::infrastructure::ai::AIClientFactory; +use bitfun_product_domains::function_agents::ports::FunctionAgentRuntimeFacade; use chrono::{Local, Timelike}; /** * Work state analyzer * * Analyzes the user's current work state, including Git status and file changes */ -use log::{debug, info}; +use log::info; use std::path::Path; use std::sync::Arc; @@ -21,229 +25,21 @@ impl WorkStateAnalyzer { ) -> AgentResult { info!("Analyzing work state: repo_path={:?}", repo_path); - let greeting = Self::generate_greeting(&options); - - let git_state = if options.analyze_git { - Self::analyze_git_state(repo_path).await.ok() - } else { - None - }; - - let git_diff = if git_state - .as_ref() - .is_some_and(|g| g.unstaged_files > 0 || g.staged_files > 0) - { - Self::get_git_diff(repo_path).await.unwrap_or_default() - } else { - String::new() - }; - - let time_info = Self::get_time_info(repo_path).await; - - let ai_analysis = - Self::generate_complete_analysis_with_ai(factory, &git_state, &git_diff, &options) - .await?; - - debug!("AI complete analysis generation succeeded"); - let summary = ai_analysis.summary; - let ongoing_work = ai_analysis.ongoing_work; - let predicted_actions = if options.predict_next_actions { - ai_analysis.predicted_actions - } else { - Vec::new() - }; - let quick_actions = if options.include_quick_actions { - ai_analysis.quick_actions - } else { - Vec::new() - }; - - let current_state = CurrentWorkState { - summary, - git_state, - ongoing_work, - time_info, - }; - - Ok(WorkStateAnalysis { - greeting, - current_state, - predicted_actions, - quick_actions, - analyzed_at: Local::now().to_rfc3339(), - }) - } - - fn generate_greeting(_options: &WorkStateOptions) -> GreetingMessage { - // Frontend uses its own static greeting from i18n. - GreetingMessage { - title: String::new(), - subtitle: String::new(), - tagline: None, - } - } - - async fn get_git_diff(repo_path: &Path) -> AgentResult { - debug!("Getting Git diff"); - - let unstaged_output = crate::util::process_manager::create_command("git") - .arg("diff") - .arg("HEAD") - .current_dir(repo_path) - .output() - .map_err(|e| AgentError::git_error(format!("Failed to get git diff: {}", e)))?; - - let unstaged_diff = String::from_utf8_lossy(&unstaged_output.stdout); - - let staged_output = crate::util::process_manager::create_command("git") - .arg("diff") - .arg("--cached") - .current_dir(repo_path) - .output() - .map_err(|e| AgentError::git_error(format!("Failed to get staged diff: {}", e)))?; - - let staged_diff = String::from_utf8_lossy(&staged_output.stdout); - let diff = super::utils::combine_git_diffs(&unstaged_diff, &staged_diff); - - debug!("Git diff retrieved: length={} chars", diff.len()); - - Ok(diff) - } - - async fn generate_complete_analysis_with_ai( - factory: Arc, - git_state: &Option, - git_diff: &str, - options: &WorkStateOptions, - ) -> AgentResult { - use super::ai_service::AIWorkStateService; - - debug!("Starting AI complete analysis generation"); - - let ai_service = - AIWorkStateService::new_with_agent_config(factory, "startchat-func-agent").await?; - ai_service - .generate_complete_analysis(git_state, git_diff, &options.language) - .await - } - - async fn analyze_git_state(repo_path: &Path) -> AgentResult { - let current_branch = Self::get_current_branch(repo_path)?; - - let status_output = crate::util::process_manager::create_command("git") - .arg("status") - .arg("--porcelain") - .current_dir(repo_path) - .output() - .map_err(|e| AgentError::git_error(format!("Failed to get git status: {}", e)))?; - - let status_str = String::from_utf8_lossy(&status_output.stdout); - - let (unstaged_files, staged_files, modified_files) = - super::utils::parse_git_status_porcelain(&status_str); - - let unpushed_commits = Self::get_unpushed_commits(repo_path)?; - let ahead_behind = Self::get_ahead_behind(repo_path).ok(); - - Ok(GitWorkState { - current_branch, - unstaged_files, - staged_files, - unpushed_commits, - ahead_behind, - modified_files, - }) - } - - fn get_current_branch(repo_path: &Path) -> AgentResult { - let output = crate::util::process_manager::create_command("git") - .arg("branch") - .arg("--show-current") - .current_dir(repo_path) - .output() - .map_err(|e| AgentError::git_error(format!("Failed to get current branch: {}", e)))?; - - Ok(String::from_utf8_lossy(&output.stdout).trim().to_string()) - } - - fn get_unpushed_commits(repo_path: &Path) -> AgentResult { - let output = crate::util::process_manager::create_command("git") - .arg("log") - .arg("@{u}..") - .arg("--oneline") - .current_dir(repo_path) - .output(); - - if let Ok(output) = output { - if output.status.success() { - let count = String::from_utf8_lossy(&output.stdout).lines().count() as u32; - return Ok(count); - } - } - - Ok(0) - } - - fn get_ahead_behind(repo_path: &Path) -> AgentResult { - let output = crate::util::process_manager::create_command("git") - .arg("rev-list") - .arg("--left-right") - .arg("--count") - .arg("HEAD...@{u}") - .current_dir(repo_path) - .output() - .map_err(|e| AgentError::git_error(format!("Failed to get ahead/behind: {}", e)))?; - - if !output.status.success() { - return Err(AgentError::git_error("No upstream branch configured")); - } - - let result = String::from_utf8_lossy(&output.stdout); - let parts: Vec<&str> = result.split_whitespace().collect(); - - if parts.len() >= 2 { - let ahead = parts[0].parse().unwrap_or(0); - let behind = parts[1].parse().unwrap_or(0); - Ok(AheadBehind { ahead, behind }) - } else { - Err(AgentError::git_error("Failed to parse ahead/behind info")) - } - } - - async fn get_time_info(repo_path: &Path) -> TimeInfo { - let time_of_day = super::utils::time_of_day_for_hour(Local::now().hour()); - - let output = crate::util::process_manager::create_command("git") - .arg("log") - .arg("-1") - .arg("--format=%ct") - .current_dir(repo_path) - .output(); - - let (minutes_since_last_commit, last_commit_time_desc) = if let Ok(output) = output { - if output.status.success() { - let timestamp_str = String::from_utf8_lossy(&output.stdout).trim().to_string(); - if let Ok(timestamp) = timestamp_str.parse::() { - let now = Local::now().timestamp(); - let diff_seconds = now - timestamp; - let minutes = (diff_seconds / 60) as u64; - - // Don't format time description here, let frontend handle i18n - (Some(minutes), None) - } else { - (None, None) - } - } else { - (None, None) - } - } else { - (None, None) - }; - - TimeInfo { - minutes_since_last_commit, - last_commit_time_desc, - time_of_day, - } + let now = Local::now(); + let git_adapter = CoreFunctionAgentGitAdapter::default(); + let ai_adapter = CoreFunctionAgentAiAdapter::new(factory); + let facade = FunctionAgentRuntimeFacade::new(&git_adapter, &ai_adapter); + // Keep the legacy analyzed_at timing in core: assign it after AI analysis completes. + let mut analysis = facade + .analyze_work_state( + repo_path.to_path_buf(), + options, + now.timestamp(), + now.hour(), + String::new(), + ) + .await?; + analysis.analyzed_at = Local::now().to_rfc3339(); + Ok(analysis) } } diff --git a/src/crates/product-domains/AGENTS.md b/src/crates/product-domains/AGENTS.md index 2aeba0d5c..b305875f1 100644 --- a/src/crates/product-domains/AGENTS.md +++ b/src/crates/product-domains/AGENTS.md @@ -42,12 +42,12 @@ moves here gradually. - `function-agents` owns pure function-agent DTOs, prompt assembly helpers, commit prompt preparation, AI-response parsing policy, diff truncation policy, local file-shape analysis, Git/AI port traits, and port-backed runtime facade - orchestration, including the commit-message facade used by core adapters. + orchestration, including the commit-message and Startchat work-state facades + used by core adapters. - Core still owns MiniApp filesystem IO, worker process execution, host dispatch execution, built-in asset seeding/source-hash lookup, `PathManager` integration, function-agent Git/AI service adapters, prompt templates, JSON - extraction, error mapping, and Startchat work-state product path wiring until - equivalence tests cover that migration. + extraction, and error mapping. ## Verification diff --git a/src/crates/product-domains/src/function_agents/ports.rs b/src/crates/product-domains/src/function_agents/ports.rs index b067e9b26..a62e4b0f6 100644 --- a/src/crates/product-domains/src/function_agents/ports.rs +++ b/src/crates/product-domains/src/function_agents/ports.rs @@ -98,9 +98,9 @@ pub trait FunctionAgentAiPort: Send + Sync { /// /// It owns only pure orchestration over function-agent ports and DTO helpers. /// Core still owns Git/AI service calls, prompt templates, JSON extraction, -/// and concrete error mapping. Startchat product-path rewiring must remain -/// blocked until its Git state, diff fallback, and time-info behavior are -/// equivalence-locked. +/// and concrete error mapping. Startchat product-path rewiring depends on core +/// adapters preserving legacy Git state, diff fallback, time-info, and +/// `analyzed_at` timing semantics. pub struct FunctionAgentRuntimeFacade<'a> { git: &'a dyn FunctionAgentGitPort, ai: &'a dyn FunctionAgentAiPort,