R02-T03 Typed Settings Facade
Khắp nơi đang viết ctx.config.routing.get("switch_mode", "off"). Gõ sai một
chữ thì lặng lẽ nhận mặc định, không ai biết cho tới lúc tính năng "không
hiểu sao không chạy". ProviderSettings / RoutingSettings / SecuritySettings
làm sai tên là lỗi ngay, và kiểu ghi rõ nên đọc là biết confirm_timeout_sec
tính bằng giây.
Là KHUNG NHÌN lên dict sống, không phải dataclass sao chép — sửa qua đây là
sửa vào cấu hình, save() là xuống đĩa, khỏi sinh chuyện đồng bộ hai chiều.
Có raw() để ai thiếu thuộc tính thì dùng tạm, đừng vòng lại config.data.
Bắt cả trường hợp giá trị là null: file cũ hay để null, đọc ra None rồi đem
so sánh số là vỡ.
R02-T06 Schema versioning + phục hồi
config.json hôm nay không có số phiên bản, nên mọi thay đổi hình dạng phải
đoán — _migrate_connectors() đoán "có khoá office nghĩa là file cũ". Giờ:
thiếu schema_version thì coi là v1, mỗi bước là một hàm chạy tuần tự, sao
lưu trước khi nâng, và file mới hơn app thì dùng nguyên trạng chứ không đoán
ngược.
R02-T05 Chuyển API key sang kho bí mật
Là bước v1→v2. Người dùng cập nhật app, mở lên, khoá cũ tự vào keyring và
biến khỏi đĩa — có test cho đúng cảnh đó.
Hai chỗ cố tình không làm:
- Máy chưa có keyring: KHÔNG chuyển, giữ nguyên v1. Thà để khoá trong file
còn hơn xoá đi rồi người dùng mất khoá mà không hiểu vì sao.
- Giá trị "ollama" là bù nhìn (Ollama đòi có api_key nhưng bỏ qua nội
dung), đẩy vào keyring chỉ tổ rác.
Hai chuỗi test trông giống khoá thật bị CASAN Check 1 bắt — đánh dấu
"# casan: allow" kèm lý do, đúng lối thoát đã thiết kế cho cả đội.
150 test xanh (129 + 21 mới). CASAN Check 1 sạch. File mới đều dưới 200 dòng.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Thay cho config.py::AppConfig. Hai khác biệt về hành vi, cả hai đều là thứ
muốn có; mọi thứ còn lại giữ y nguyên vì đây là refactor.
1. Ghi qua AtomicJsonFile — mất điện giữa lúc lưu không còn làm hỏng cấu hình.
Có test riêng ở tầng này chứ không chỉ dựa vào test của AtomicJsonFile.
2. Đường A (chốt 21/08): provider_conf() đọc khoá từ SecretStore rồi ghép vào
dict trả về, còn set_api_key() ghi khoá vào kho và để chuỗi rỗng trên đĩa.
Kết quả: 5 nơi đang đọc conf["api_key"] không sửa dòng nào — 3 trong đó
thuộc providers/ của Team Duy — mà file JSON vẫn sạch để qua CASAN Check 1.
Hai test riêng cho đúng hai vế đó.
provider_conf() trả BẢN SAO. Nếu trả tham chiếu thì khoá vừa ghép vào sẽ lẫn
ngược vào self.data rồi theo save() xuống đĩa — đúng thứ đường A phải tránh.
Có test cho chuyện này.
secrets=None thì lùi về hành vi cũ (khoá nằm trong file). Cần vậy để chuyển
dần ở R02-T05 chứ không phải đổi một phát cả app, và để máy không có keyring
vẫn chạy.
Giữ nguyên có chủ đích: trộn sâu với mặc định, biến môi trường, và
ms365.unlocked không bao giờ chạm đĩa — mỗi thứ một test.
_deep_merge chép lại 6 dòng thay vì import từ config.py: file này phải sống
được sau khi config.py biến mất.
129 test xanh (119 + 10 mới). CASAN Check 1 sạch. File mới: 188/102/86 dòng,
đều dưới ngưỡng 400.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Ngày 21/08 của làn N1 (Nam): R02-T01 và R02-T04.
--- Lỗi phải sửa trước khi làm được gì ---
Kế hoạch đặt tên một tầng là platform/. Tôi dựng đúng theo đó sáng nay, có
kiểm "platform stdlib không bị che" và báo là an toàn. Kiểm đó SAI: tôi chỉ
thử từ thư mục cha. Chạy từ gốc repo — đúng cách 26 script trong tools/ và
scripts/ được gọi — thì platform/ che khuất platform của thư viện chuẩn, và
import keyring chết ngay:
AttributeError: module 'platform' has no attribute 'system'
Nghĩa là R02-T04 không thể làm được chừng nào thư mục đó còn tên cũ. Đổi
platform/ -> adapters/. Đây là lệch khỏi plan.md và ảnh hưởng Team Hoa (họ sở
hữu platform/qt/qt_scheduler_clock.py) — đã ghi vào GammaTeam_decisions.md.
tests/test_no_stdlib_shadow.py chặn lỗi tái diễn, hai lớp: một bài so tên thư
mục gốc repo với sys.stdlib_module_names, một bài chạy tiến trình con với cwd
là gốc repo rồi import keyring thật. Dựng lại platform/ là cả hai đỏ.
--- R02-T01: AtomicJsonFile ---
config.py::save() đang gọi path.write_text(), tức là cắt file về 0 byte rồi
mới ghi. Chết giữa chừng là mất sạch cấu hình. Thay bằng: ghi file tạm cùng
thư mục -> flush + fsync -> os.replace (nguyên tử trên cả Windows và POSIX).
Test tiêm lỗi đúng như cột nghiệm thu của plan.md: cho os.replace ném lỗi
ngay bước cuối rồi khẳng định file cũ còn nguyên. Chỉ test "ghi rồi đọc lại"
thì write_text() cũ cũng qua — mà đó chính là thứ đang thay.
Phần đọc: file hỏng được dời thành .bad-<thời điểm> rồi trả mặc định. Giữ
đúng hành vi "hỏng cấu hình không chặn khởi động" của config.py, thêm phần
cứu được bản hỏng.
--- R02-T04: KeyringAdapter ---
Windows Credential Manager / macOS Keychain / Linux Secret Service. Không bao
giờ ném lỗi: máy không có kho (Linux headless, CI) thì available=False và trả
None, để tầng UI nói "chưa lưu được khoá" thay vì sập app. Test tiêm backend
giả, không đụng keyring thật của máy chạy test.
119 test xanh (102 + 17 mới). CASAN Check 1 sạch.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
EPIC R06 (Team Hoa) - workspace/filesystem isolation, no cross-project
mutable state.
R06-T01 domain/workspaces/workspace_session.py
WorkspaceSession - project_id/workspace_root/sandbox_dir/allowed_paths
frozen snapshot + is_allowed(path), same "capture once at submit time"
shape as R04's ConversationExecutionRequest.
R06-T02 infrastructure/persistence/json/{atomic_write,workspace_repository_impl,conversation_repository_impl}.py
Real bug fixed: core/projects.py::save_project and core/history.py's
save_conversation/rename_conversation/set_pinned did a plain
path.write_text(json.dumps(...)) - two syscalls, no atomicity. A crash
between them leaves a half-written file that load_project/load_conversation
then silently treat as "missing". All four now write through
atomic_write.write_json (temp file + os.replace). WorkspaceRepository/
ConversationRepository are thin object-shaped facades over the same
(now-atomic) functions, for future application-layer callers.
NOTE: atomic_write.py is deliberately NOT named atomic_json_file.py -
R02-T01 (Team Nam) claims that filename for the same purpose app-wide;
see the checklist for the consolidation TODO.
R06-T03 infrastructure/filesystem/execution_workspace.py
ExecutionWorkspace names the output_dir/scratch_dir split that already
exists (core/chat_agent.py's flat workspace_root/.scratch) - does not
move anything.
R06-T04 ui/chat_panel.py
The actual race: ChatPanel._persist_session (saves a BACKGROUND turn's
conversation) resolved its save directory via a live
self.ctx.config.history_dir() read at save time. ui/workspace_tab.py::
_load_current mutates that same config field on every project switch, so
a turn still running when the user switched projects got saved into the
NEW project's history folder. Fixed by adding "home_history_dir" to the
per-turn ctx dict (same "home_*" snapshot convention already used for
session id/messages/title), captured at submit time. Verified with a real
offscreen-Qt test, not just a unit double:
tests/integration/test_history_dir_race.py.
R06-T05 application/workspaces/file_workspace_service.py
FileWorkspaceService - the File Explorer / AI Editor entry point for the
same safe read/write/edit operations the agent tool loop has, by calling
core/tools.py::execute_tool directly (same dispatch, same ToolContext
containment, same audit log) rather than reimplementing any of it.
New tests: tests/unit/test_workspace_session.py,
test_atomic_write_and_repositories.py, test_execution_workspace.py,
test_file_workspace_service.py, tests/integration/test_history_dir_race.py
(29 new tests, incl. 2 real offscreen-Qt integration tests).
Suite: 283 passed, 4 pre-existing failures unrelated to R05/R06 (see
checklist). check_imports: PASS. All new files < 400 LOC.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Đổi mô hình: không còn nhánh riêng mỗi người, cả ba cùng đẩy vào
gamma/refactor. Ba "nhánh" thành ba "làn" — vẫn chia việc như cũ, nhưng ranh
giới file bây giờ là thứ DUY NHẤT giữ ba người không giẫm chân, vì không còn
nhánh riêng làm vùng đệm.
Thêm quy ước số 4 cho nhánh chung, xếp vào nhóm bắt buộc: pull --rebase trước
mỗi lần đẩy; commit nhỏ, đẩy trong ngày; không bao giờ đẩy thứ làm
pytest tests -q đỏ, vì nhánh hỏng là hai người kia đứng hình.
Phần nghiệm thu đổi theo: trước đây so file giữa ba nhánh, giờ không còn ba
nhánh để so. Thay bằng git log --name-only --pretty=%an trên gamma/refactor —
không file nào được xuất hiện dưới hai tên khác nhau.
Hai quyết định đã chốt, ghi vào GammaTeam_decisions.md:
1. api_key: đường A — ConfigRepository ghép key từ SecretStore vào dict, 5
nơi đọc không đổi dòng nào, không cần báo Duy và Hoa.
2. 24 checker UI: đường A — ai dời file thì sửa checker ngay trong commit
đó, kèm ràng buộc phải nói rõ sửa gì và chạy check_probes_bite.py sau.
Không đưa vào CI sprint này vì chúng dựng MainWindow thật.
Baseline trong tài liệu cập nhật 90 -> 102 test.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
EPIC R05 (Team Hoa) - one security/approval path for every tool call.
R05-T01 domain/tools/{tool_descriptor,tool_registry}.py
ToolCapability (READ/WRITE/EXECUTE/NETWORK, composable) + ToolDescriptor +
ToolRegistry, replacing three independently-maintained gating lists
(core/tools.py::WRITE_TOOLS, code_agent.py's WRITE_TOOLS|MS365_WRITE_TOOLS,
chat_agent.py's literal ("run_command","install_package") tuple) with one
capability lookup.
R05-T02 infrastructure/filesystem/{file_tools,command_tools,fetch_tools,tool_context}.py
core/tools.py's execute_tool if/elif chain split into per-concern modules.
core/tools.py is now a strangler-fig shim: re-exports ToolContext/ToolError,
dispatches through a {name: handler} dict built from the split modules.
core/tools.py: 566 -> 291 lines.
R05-T03 application/conversations/tool_policy_gateway.py
ToolPolicyGateway.allow(name, gate, payload) - capability-driven ALLOW vs
ask-the-gate decision. Wired into both chat_agent.py::run_cowork and
code_agent.py::run_code, replacing their separate hand-rolled checks.
Verified equivalent to the old hardcoded sets by test.
R05-T04 (behavior change, not just refactor)
MCP/connector tools (core/mcp_client.py, core/ext_connectors.py) reached
chat_agent.py via extra_executor(name, args) with NO permission check at
all. They are now tagged with a conservative default capability
(WRITE|EXECUTE|NETWORK - no MCP tool self-declares risk) and routed through
the SAME ToolPolicyGateway as built-ins. When "confirm before running
commands" is on, MCP/connector calls now prompt like run_command already
did - a real gap closed, and a user-visible change worth calling out.
R05-T05 infrastructure/mcp/mcp_source_manager.py
McpToolSourceManager extracts the connection cache/lock/start-or-skip
lifecycle out of state.py::AppContext (_mcp_connections/_conn_lock) into a
standalone, directly-testable class. AppContext.build_mcp_tools and
_ms365_builtin_connection now call ensure()/stop(); _ext_connections
(unified Connectors) is out of scope for this task and keeps its own lock.
New tests: tests/unit/test_tool_registry_and_policy.py,
test_code_agent_tool_policy.py, test_cowork_extra_tool_policy.py,
test_mcp_source_manager.py (26 new tests).
Suite: 254 passed, 4 pre-existing failures unrelated to R05 (2 EPIC R02
config-security, 2 environment-dependent routing tests - see checklist).
check_imports: PASS. All new files < 400 LOC.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
N3 (Co4E) cần gọi tool nhưng Team Hoa chưa bắt đầu. Ba đường: N3 ngồi đợi
(trái nguyên tắc không team nào chặn team nào), N3 tự phỏng đoán (không ai
soi, chắc chắn phải sửa), hoặc viết một bản đề xuất để Hoa duyệt. Chọn cái
thứ ba.
Ranh giới giữ đúng sơ đồ phân hệ trong plan.md: domain/security/ là của
Gamma, application/conversations/tool_policy_gateway.py là của Hoa. Nên Gamma
định nghĩa hình dạng, Hoa cài đặt. Không đụng file nào của họ.
Hình dạng bám vào code đang chạy: SecurityVerdict (allowed/reason/layer) và
hộp thoại xin phép ở chat_panel.py:1312. Khác biệt duy nhất là gộp thành một
câu trả lời ba trạng thái ALLOW/DENY/ASK, thay vì bắt chỗ gọi tự nhớ hỏi hai
nơi.
Hai ràng buộc đưa vào có chủ đích, mỗi cái một test:
- DENY và ASK bắt buộc có reason, ném lỗi ngay lúc dựng. Người dùng cần
biết vì sao bị chặn và audit_log cần ghi lại.
- ASK không phải allowed. Đây là bẫy dễ mắc nhất: coi ASK như ALLOW thì
tool chạy trước khi có ai đồng ý.
Kèm FakeToolPolicyGateway lập trình được theo tên tool hoặc theo hàm, có ghi
lại đã hỏi những gì — test khẳng định được "có hỏi cổng không", không chỉ
"kết quả đúng không".
docs/refactor/GammaTeam_decisions.md thêm quyết định 3, kèm nguyên văn tin
nhắn cần gửi Hoa và ô đánh dấu đã gửi / đã xác nhận.
102 test xanh (96 + 6 mới). CASAN Check 1 sạch. domain/ và application/ có 0
import PySide6 — kiểm bằng AST, vì grep đếm ra 4 mà cả 4 là chữ "PySide6"
nằm trong chính docstring cảnh báo. Check 3 của Team Duy nên phân tích cú
pháp chứ đừng grep.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Ba khối, mỗi người một khối: cột trái là thứ phải có trong tay mới làm được kèm
nguồn, cột phải là thứ bắt buộc giao ra kèm người nhận. Nhãn 'có rồi' đánh dấu
những gì mục chung đã giao xong hôm nay (SecretStore, ConfigRepository, fake,
script CASAN).
Kèm bảng output bắt buộc với cả ba mỗi PR, mỗi dòng có lệnh tự kiểm.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sáu việc trong "mục chung" của bản phân công, làm trước khi ba nhánh tính năng
tách ra.
1. Khung 5 tầng theo đúng đường dẫn plan.md: domain/ application/
infrastructure/ presentation/ platform/ + tests/fakes/ — 38 __init__.py.
Trước đó là 0 file, mà mọi task của cả ba người đều ghi vào đây.
Đã kiểm platform/ không che khuất module platform của stdlib.
2. Hợp đồng SecretStore và ConfigRepository (Protocol, chưa cài đặt) + fake
chạy trong bộ nhớ. Danh sách thuộc tính không bịa: đếm 156 lời gọi
ctx.config.* trong 29 file rồi lấy những cái dùng thật, xếp theo số lần.
Cố ý bỏ config.data (36 lời gọi, nhiều nhất) — bê dict thô sang kiến trúc
mới là bê nguyên vấn đề cũ.
3. tests/test_contracts.py — bài nghiệm thu, không phải test cho vui. Bài
chính chạy tiến trình riêng và khẳng định dùng fake KHÔNG kéo theo
cowork_local.config lẫn PySide6; đó là điều kiện để N2 và N3 code ngay hôm
nay thay vì đợi bản thật ngày 23 và 26/08.
4. scripts/audit_security.py — CASAN Check 1, Gamma chủ trì (hạn 30/08). Viết
sớm để kiểm liên tục trong lúc chuyển API key, không đợi tới ngày cổng.
Lần chạy đầu ra 3 báo động giả (secret_in_output là tên quy tắc, api_key="x"
là dữ liệu test) nên đã siết: ngưỡng độ dài, hằng liệt kê, hình dạng khoá
i18n, và dấu "# casan: allow" làm lối thoát chuẩn.
--self-test cắm 4 credential thật + 5 mẫu vô hại để chứng minh nó còn cắn
được — một máy quét không tìm thấy gì chỉ có giá trị nếu chứng minh được nó
biết tìm.
5. Ba check CASAN vào CI, chạy mọi PR thay vì dồn tới 30/08. Check 2 và 3
thuộc Team Hoa và Team Duy, chưa có script — bước CI bỏ qua nếu file chưa
tồn tại, để thêm cổng không làm đỏ CI của hai team kia.
6. docs/refactor/GammaTeam_decisions.md — hai quyết định chờ nhóm trưởng chốt:
provider_conf() còn trả api_key hay không (ảnh hưởng 5 nơi, 3 nằm ngoài
team), và số phận 24 checker UI sẽ vỡ khi file bị dời.
96 test xanh (90 cũ + 6 mới). CASAN Check 1: 0 credential lộ.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Trang HTML tự đứng một mình, mở bằng trình duyệt là xem được, không cần mạng.
Chia toàn bộ phần việc của team trong plan.md (R02, R07-T06, R08-T07…T10, R09,
CASAN Check 1) cho 3 người:
- Một mục chung nhóm trưởng làm trước, xong mới chia nhánh: dựng khung 5 thư
mục đích (hiện là 0 file), interface + fake cho Config/Secrets, chốt số phận
api_key, script CASAN Check 1, đưa 3 check vào CI, quyết số phận 24 checker
UI sẽ vỡ khi file bị dời.
- Ba nhánh tính năng ngang nhau, mỗi nhánh ~2.700 dòng: N1 cấu hình và vỏ ứng
dụng (nhóm trưởng giữ, vì chạm app.py / config.py / theme.py / i18n.py),
N2 giám sát, N3 Co4E.
- Bảy quy ước cho N2 và N3, ba trong đó là bắt buộc.
Số dòng code, 156 lời gọi ctx.config, 24 lời gọi audit_log.record và baseline
90 test đều đo trực tiếp trên main ngày 21/08, không lấy từ tài liệu.
Footer ghi rõ phần nào là đề xuất, phần nào lấy từ ba tài liệu gốc — mục chung,
cách chia nhánh, quy ước và nghiệm thu là đề xuất.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
docs/refactor/BaoCao_TeamDuy_R01_R03_R04.md records what was delivered against
each of the 16 tasks, the measured evidence (243 tests, 218 of them in 1.22s;
check_imports PASS; no production file over 400 LOC), the three real defects
found while working - the routing_application() deadlock, the swallowed
"notice" event, and the suite silently testing a different checkout - plus the
six open decisions and, explicitly, what was NOT tested (no manual app launch,
no real provider traffic, tools/check_*.py not run).
Refactoring_Checklist.md now links to it from the progress block.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The previous commit recorded Team Duy as owning R01/R02/R04/R10. That is wrong.
Feature_Architecture_Proposal.md line 7 and DeltaTeam_prompt.md line 17 both
state R01, R03, R04, R08 (Chat UI) and R10; R02 belongs to Team Nam, which is
also who owns the two failing config-security tests.
The completed work itself (R01, R03, R04) was already correct and is unchanged.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Verification gap closed. The suite proved the new services correct in isolation,
but three paths I had modified had no test actually running them:
tests/integration/test_task_executor_flow.py (7 tests)
The Schedule Task path after R04-T05. Pins that History is still re-saved from
the LIVE message list mid-run (the reason begin_turn() exists - the pre-turn
copy would have frozen progress at the first user message), that update_plan
tracking still reports an unfinished checklist, and that a failed run still
raises so execute_task writes error.txt.
tests/integration/test_routing_surfaces.py (11 tests)
Real offscreen CoworkTab/Co4ETab/FolderTab calling the shared routing service:
correct surface key per screen, Auto switches, Off does not consult the engine,
Manual switches only on approval, a pinned Admin agent still wins, and AI-Edit
still pins TaskType.CODING. Also pins the field contract ui/routing_toggle.py
reads off RoutingDecision (from_model/to_model as provider/model keys) - a
rename there would only fail inside a modal dialog.
Also updates docs/refactor/Refactoring_Checklist.md: the 16 completed R01/R03/R04
tasks, the Team Duy daily rows, and a status block recording the measured
numbers, the scope correction (team owns R01/R02/R04/R10), and what is still
outstanding.
Suite: 243 passed, 2 pre-existing failures (EPIC R02). Fast suite (unit +
contracts + characterization + routing): 218 passed in 1.16s.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
EPIC R04 (Team Duy) - the turn lifecycle leaves the widget.
R04-T01 domain/agents/conversation_execution_request.py
Frozen snapshot of one turn, captured on the UI thread at submit time. The
job closure used to read widget/workspace state from inside the worker
thread, so a turn could run on a mix of submit-time and later state
depending on thread timing.
R04-T02 domain/agents/agent_event.py
13 frozen event types replacing untyped emit() dicts, with a two-way bridge
so existing widgets keep consuming the legacy shape until EPIC R08. Adds
TurnCompletedEvent - the end-of-turn signal the engine never had, which is
why a cancelled turn and a failed turn look identical to the UI today.
R04-T03 application/conversations/conversation_application_service.py
Runs a turn from a request and reports typed events. Never raises across the
worker boundary; TurnResult.raise_if_failed() preserves the existing
exception-based failure path. begin_turn()/execute_turn() expose the live
message list for callers that autosave history mid-run.
R04-T04 ui/cowork_tab.py::build_job -> snapshot + service.
R04-T05 core/task_executors.py::_run_agent -> same service (was a second,
slightly different assembly of the same call).
Caught while wiring the bridge: the first event vocabulary had no "notice"
event, so Agent Security warnings and auto-compaction notices would have been
silently swallowed. Added NoticeEvent plus a test that scans the engine sources
for emit() tags and fails when one has no typed counterpart.
New: tests/integration/ - real offscreen CoworkTab running a scripted turn end
to end (7 tests), including a characterisation of the extra provider call Agent
Security spends reviewing each request.
Suite: 225 passed, 2.74s. check_imports: PASS. All new files < 400 LOC.
2 pre-existing failures remain in test_config_security.py (EPIC R02/Team Nam).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
EPIC R03 (Team Duy) - one provider catalogue, one routing flow, one usage seam.
R03-T01 tests/contracts/test_providers.py
29 contract tests every provider must satisfy: canonical assistant message,
streamed text == returned content, reasoning never joins the answer, parsed
tool arguments, ProviderError for every failure. Real adapters exercised
offline by stubbing Provider._request.
R03-T02 domain/models/provider_descriptor.py
infrastructure/providers/provider_registry.py
Provider facts declared once (was split across providers/factory.py,
DEFAULT_CONFIG and PROVIDER_LABELS). ProviderRegistry.build() also stamps the
descriptor id onto the instance, so ollama/github_copilot/codex usage is no
longer all attributed to "openai_compat", and never mutates the caller config.
R03-T03 application/model_routing/routing_application_service.py
Pure-Python routing policy with four modes: Off, Auto, Manual and the new
Fallback (switch only AFTER the current model fails). Depends on a RoutingPort
protocol; production wires the existing core.routing engine underneath.
R03-T04/T05 ui/chat_panel.py, ui/co4e_tab.py, ui/folder_tab.py
Three near-identical routing copies (~40 lines each) replaced by a call to
ctx.routing_application() plus a confirm callback. Mode vocabulary now lives
in one place (normalize_mode/is_valid_mode) instead of four literal tuples.
R03-T06 infrastructure/telemetry/usage_sink.py
Token usage extracted from both providers into UsageEvent + UsageEventSink.
Estimation pinned against core.usage_tracker so no recorded number changes.
Also fixes a deadlock introduced while wiring AppContext: routing_application()
held _routing_lock and called routing(), which takes the same non-reentrant lock.
Suite: 186 passed, 1.22s. check_imports: PASS. All new files < 400 LOC.
2 pre-existing failures remain in test_config_security.py (EPIC R02/Team Nam).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
EPIC R01 (Team Duy) - safety net before the parallel refactor starts.
R01-T01 docs/architecture/ADR-001-layered-architecture.md
4-tier boundaries, allowed dependency directions, invariants I1-I6 and
the strangler-fig migration strategy.
R01-T02 tests/fakes/{fake_provider,fake_tool_executor}.py
Scripted, offline Provider and extra-tool executor doubles.
R01-T03 scripts/check_imports.py
AST-based Clean Architecture Guard (CASAN Check 3). Also covers relative
imports and function-local imports; ASCII-only output for cp932 consoles.
R01-T04 tests/characterization/test_run_cowork.py
13 snapshot tests pinning run_cowork's current observable contract before
EPIC R04 moves its orchestration into application/.
R01-T05 docs/architecture/dormant-code.md
Import-graph scan: 43 unimported modules verified down to 6 genuinely
dormant items (~1887 LOC); the rest run via subprocess/CLI entry points.
tests/conftest.py binds `cowork_local` to THIS checkout by absolute path -
previously sys.path discovery could import a sibling checkout and the suite
would silently test the wrong code.
Suite: 104 passed, 1.08s (2 pre-existing failures in test_config_security.py
remain - config.py still ships a hardcoded default password, EPIC R02/Team Nam).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copy of sections VI-IX from Feature_Architecture_Proposal.md
(roadmap, team assignment/KPI, anti-patterns, function migration map).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>