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>
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 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>
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>
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>
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>
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>
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>
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>
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>
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>
Đổ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 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>
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>
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>
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>
Mô tả hệ thống ĐANG CHẠY, không phải hệ thống mong muốn. Mọi khẳng định chỉ
tới file:dòng cụ thể, và mỗi tham chiếu đã được kiểm bằng script: mở đúng file,
đọc đúng dòng, đối chiếu nội dung có khớp điều đang nói không. Lần kiểm đầu bắt
được 3 tham chiếu thiếu tiền tố core/ và 2 số dòng lệch — dòng 249 là "No-op
for any other tool", câu về bộ phân loại luôn bật nằm ở 250.
Bốn điểm đáng chú ý trong tài liệu:
- Đây KHÔNG phải rào chắn an ninh. Chính agent_security.py nói vậy ở đầu
file, và hệ quả là mọi tầng AI đều mở khi hỏng. Ai đọc để đánh giá rủi ro
phải hiểu đúng chỗ này.
- Phân biệt quy tắc xác định và quy tắc do AI phán. Tắt hết công tắc trong
màn Cài đặt thì VẪN còn bộ phân loại mẫu và sandbox — đây là điểm dễ hiểu
nhầm nhất, vì mấy công tắc đó chỉ tắt phần AI.
- Trạng thái thứ ba: hỏi người dùng. Hệ thống đã có (chat_panel.py:1312) mà
chưa gọi tên; tool_policy.py gộp thành ALLOW/DENY/ASK.
- Mục 8 liệt kê 4 chỗ đã biết là yếu, để người sau khỏi tưởng đã kín: mở khi
hỏng, bí mật vẫn đi trong bộ nhớ (hệ quả của đường A), bộ luật OneDrive
không ký số, và ASK chưa nối được vào Co4E.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
EPIC R03 (Team Duy) — Model Providers & Routing. All six tasks done.
R03-T02 — Provider catalogue
domain/models/provider_descriptor.py ProviderDescriptor (frozen), WireProtocol, AuthKind
infrastructure/providers/provider_registry.py
thread-safe registry: id/alias lookup, dynamic
lookup by model id, adapter selection by protocol
providers/factory.py drops its own _REGISTRY table and delegates to the
registry, still raising ProviderError for callers
R03-T03 — RoutingApplicationService (pure Python, 4 modes)
application/model_routing/routing_models.py
RoutingMode (off/auto/manual/fallback),
RoutingRequest (immutable snapshot), RouteEvaluation,
RoutingOutcome
application/model_routing/routing_application_service.py
the single decision flow, reached through two narrow
ports plus a caller-supplied confirm callback, so no
Qt import is needed
application/model_routing/core_routing_adapter.py
binds the ports to core/routing and AppContext
Fallback is a new resilience mode: keep the selected model while it can serve the turn,
re-route only when it cannot. Wired end to end through config.py, state.py,
ui/routing_toggle.py and i18n.py (EN/JA/VI).
R03-T04 / T05 — Remove the duplicated routing flow
ui/chat_panel.py (#L638), ui/co4e_tab.py, ui/folder_tab.py each drop ~35 lines of copied
logic and call the shared service; the widgets now only build a RoutingRequest, host the
Manual-mode modal and render the outcome.
R03-T06 — Token usage as an event
infrastructure/telemetry/usage_sink.py UsageEvent + UsageEventSink protocol, with tracker,
in-memory and composite sinks
providers/openai_compat.py, providers/anthropic.py
publish a UsageEvent instead of writing to the
usage tracker themselves
core/usage_tracker.py adds current_context() so a sink can borrow and
restore a thread's attribution
R03-T01 — Contract tests
tests/contracts/test_providers.py parametrises over every provider in the registry: chat()
signature, canonical assistant message, normalised tool calls, response closed, tool schema
translation, ProviderError, list_models/test_connection, one UsageEvent per turn.
Test infrastructure fix (required to verify any of the above): tests/conftest.py used to put
the repository's PARENT directory on sys.path, so `import cowork_local.*` resolved against
whichever sibling folder happened to carry that name — on a dev machine, an unrelated older
checkout. The suite reported green while exercising different code. The conftest now binds
this checkout to the cowork_local name in sys.modules.
Verification
pytest tests/ 236 passed in ~1.8s (102 before this change)
scripts/check_imports.py PASS, 0 forbidden imports in domain/ and application/
new production files largest is 288 lines, all under the 400 LOC ceiling
new tests 134 (50 contract, 70 unit, 14 integration), all offline
scripts/run_quality_gate.py does not exist yet (R10-T02), so DoD item 7 was covered by
check_imports.py plus the full suite.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Dòng 31 ghi `secrets/`. Mẫu không neo, nên git bỏ qua MỌI thư mục tên
secrets ở mọi độ sâu — kể cả infrastructure/secrets/ vốn là mã nguồn.
Ba file ở đó chưa bao giờ lên repo. Máy tôi vẫn 150 test xanh vì pytest
đọc đĩa chứ không đọc git; ai clone sạch thì đỏ 4 file ngay lúc thu thập:
ModuleNotFoundError: No module named
'cowork_local.infrastructure.secrets'
Hiệp phát hiện, không phải tôi. Đã dựng lại bằng clone sạch vào thư mục
đặt đúng tên cowork_local để tái hiện.
Neo mẫu thành /secrets/ và thêm tests/test_no_ignored_source.py — hỏi
thẳng git chứ không hỏi đĩa, nên lần sau lỗi cùng hình dạng sẽ đỏ ngay
trên máy người viết. Đã kiểm ngược: trả lại `secrets/` thì cả ba bài đỏ.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sau khi vá secrets/ thì rà cả file xem còn mẫu không neo nào sắp cắn hai
người kia. Còn hai quả đang sống:
logs/ -> nuốt infrastructure/logs/ (Hiệp làm CanonicalAuditLogger,
đây là tên rất dễ đặt)
build/ -> nuốt application/*/build/
dist/, out/ cùng kiểu
Chưa ai vấp, vá trước. Trong repo không có build//dist//out//logs/ lồng
nhau nào nên neo về gốc không mất gì — đã kiểm hai chiều: đường dẫn mã
nguồn qua được, còn build/x.o, dist/app.exe, logs/run.log ở gốc vẫn bị
chặn như cũ.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Mirrors docs/refactor/BaoCao_TeamDuy_R01_R03_R04.md's structure: per-EPIC
results, test evidence, the two real bugs found and fixed, secondary
improvements, open items needing another team's sign-off, untested scope,
and what's next.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
`tests/test_project_context_mcp_template.py` imported `mcp` at module scope,
but the SDK is a runtime dependency (requirements.txt) and is deliberately
absent from requirements-test.txt — the only thing CI installs. Collection
therefore aborted for the ENTIRE suite before a single test ran.
The guard now sits inside the one test that touches the SDK, so the other
cases in the file (pure-Python contract checks) keep running on CI instead
of being skipped along with it.
Unrelated to the R04 refactor; kept as its own commit so it can be cherry-
picked to main on its own.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
R04-T01 — `domain/agents/conversation_execution_request.py`: a frozen
snapshot of everything one chat turn needs. Turn inputs previously lived in a
closure plus a 15-key ctx dict inside `ui/chat_panel.py::_start_turn`, and the
worker thread kept reading the widget back while it ran, so every later click
was visible to work already in flight. The request also owns the prompt
composition rules (instruction prefix separator, session notes, model-switch
review note) that were inline in that closure.
R04-T02 — `domain/agents/agent_event.py`: 13 frozen event types replacing the
untyped `{"type": ...}` dicts, whose only specification was the 130-line
if/elif chain in `_on_event`. Each event serialises back to the exact legacy
dict, so the presentation layer is untouched; `agent_event_codec.py` parses the
other way and is a temporary shim, isolated so R08 can delete it in one move.
`assistant_done` is deliberately NOT the end of a turn (it fires once per
provider call), so it maps to AssistantMessageCompletedEvent while the new
TurnCompletedEvent reports the turn itself.
R04-T03 (part) — `domain/agents/agent_result.py`: one named outcome for a
finished turn, replacing the message list / 3-tuple / reconstructed-from-side-
effects trio the three callers each read differently.
Verification: 66 tests. Beyond the unit tests,
`tests/integration/test_agent_event_bridge.py` runs the REAL `run_cowork` loop
offline and asserts every dict it emits is recognised and round-trips
byte-for-byte — a guard against an event type nobody modelled or a key whose
meaning silently drifted.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
R04-T03 — the turn lifecycle, extracted from `core/chat_agent.py::run_cowork`
into `application/conversations/`. The 260-line body mixed the lifecycle (step
budget, cancel checks, guard -> preview -> gate -> execute ordering, sandbox
tidy-up) with the machinery doing each step, and reaching any of it meant
standing up a Qt widget and a worker thread. It is now a plain object driven
through two Protocols and six callables (`turn_runtime.py`), with the concrete
`core/*` wiring confined to `core_runtime_adapter.py` — the same shape R03 used
for routing. Faithful port, not an improvement pass: where the original had a
quirk (the step-ceiling note only merges into the answer when the last message
is the assistant's) the quirk is preserved and commented.
R04-T04 — `ui/cowork_tab.py::build_job` no longer calls run_cowork. It captures
the widget's state at submit time, builds the request via the new
`cowork_turn_request.py` and executes it. `execute(..., messages=...)` hands the
widget's own list over because `_reattach_running_turn` replays from it WHILE
the worker appends and `_finalize_turn` slices it afterwards — a private list
would break both silently.
R04-T05 — `core/task_executors.py`'s cowork branch shares the same engine. All
five unattended-run behaviours stay put (plan reminder, history_ready, History
autosave per assistant message, timeout notice, plan_incomplete_reason), and
`_unattended_prompt` now expresses the load-bearing prefix order in one
readable call instead of three successive rebindings.
Verification: 74 new tests (364 passed, 1 skipped overall; check_imports PASS).
The two that matter most:
- `test_conversation_service_parity.py` runs the same scripted turn through
run_cowork AND the service and compares the event stream, the resulting
conversation and the advertised tool list across 7 scenarios;
- `test_task_executor_turn.py` was written BEFORE the migration and passed 8/8
against the old code, then unchanged against the new.
Known: `ui/cowork_tab.py` (416 -> 455) and `core/task_executors.py` (476 -> 524)
stay above the 400-LOC limit. Both were already over it before this change;
bringing them under needs the R08 / R07 decompositions.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Nam chốt: không chờ Delta merge vào main, lấy sớm để va chạm nhỏ và sửa
ngay, thay vì dồn một cục lúc cả hai cùng lên main.
R04 chứa trọn R01 và R03 nên một lần merge là đủ cả ba: 96 file, +8260
dòng. Xung đột chỉ 5 file, đều là __init__.py add/add — hai team cùng
dựng khung thư mục nên đụng docstring. Giữ docstring của Gamma (nói rõ
ràng buộc "không import PySide6"), giữ mọi phần code của Delta.
Riêng tests/fakes/__init__.py: bỏ hai dòng import háo hức của Delta
(fake_provider, fake_tool_executor). fake_provider dùng
`from providers.base import ...` — import tuyệt đối, chỉ chạy được khi
cwd là gốc repo — nên nó làm đứt bài test "dùng fake mà không nạp config
thật". Không ai import ở cấp package; test của Delta gọi thẳng module
nên bỏ đi không ảnh hưởng họ. Đã ghi lý do vào docstring của gói.
Delta cũng xoá preview-desktop và "requirements (cloud copy).txt".
430 test xanh sau merge.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Bắt được nhờ merge Delta: bộ test của họ chạy lâu hơn nên lộ ra một bài
của tôi chập chờn. Truy ra không phải lỗi test mà là lỗi thật trong code
chạy máy người dùng:
PermissionError: [WinError 5] Access is denied
.dem.json.l7x2a8pd.tmp -> dem.json
MoveFileEx trả ERROR_ACCESS_DENIED khi tiến trình khác đang giữ handle
lên nguồn hoặc đích — trên Windows gần như luôn là Defender hoặc Search
Indexer quét file vừa tạo, giữ vài chục mili-giây rồi nhả.
Đo được: hỏng 1 trong 7 lượt chạy 20 lần ghi, tức khoảng 1 trên 140 lần
lưu. Nghĩa là người dùng thỉnh thoảng bấm Lưu là văng lỗi, và không tài
nào tái hiện được để báo.
Thêm vòng thử lại 6 lượt, nghỉ tăng dần 20ms → 640ms. Hết lượt vẫn ném
lỗi, không nuốt lỗi quyền thật, và luôn dọn file tạm.
Hai bài test mới, đã kiểm ngược: bỏ vòng thử lại thì bài thứ nhất đỏ.
Chạy lại 30 lượt sau khi vá: 0 hỏng (trước khi vá: 4).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Lane N3 (Co4E Studio) — dùng bộ workflow refactor-god-file, mỗi bước có
characterization test trước khi tách, hậu kiểm ranh giới tầng sau mỗi bước:
- skills_list_panel.py / agent_list_panel.py — 2 khu vực sidebar
- co4e_canvas_widget.py + canvas_items.py + canvas_interaction_mixin.py —
Co4ECanvas tách 3 file (vượt 400 dòng nếu đứng một mình)
- node_property_panel.py + node_property_actions_mixin.py +
step_config_section.py — StepConfigPanel, cùng lý do
- co4e_run_control_widget.py — RunsPagePanel (trang Flow Status)
- co4e_chat_view.py — ChatPanel + _ChatInput + helper autocomplete
- palette_list.py — _PaletteList dời khỏi ui/co4e_tab.py, hết import ngược
presentation -> ui (agent/skills panel giờ import top-level)
ui/co4e_tab.py giảm 2089 -> 1878 dòng, chỉ còn phần wiring + business logic
(Co4ERunManager/AgentWorker chưa đổi — nằm ngoài phạm vi này, xem docstring
presentation/co4e/co4e_tab.py). ui/co4e_canvas.py và ui/co4e_config_panel.py
còn lại là compat shim re-export, không đổi API cho bên gọi.
Thêm tests/test_co4e_integration.py — dựng thật Co4ETab qua build_co4e_tab(),
lái luồng qua nhiều panel trong cùng instance (thêm node, mở/gập chat, chuyển
trang Flow Status rồi quay lại không mất state canvas) — bắt lỗi wiring
xuyên-panel mà characterization test từng panel riêng không thấy được.
Đã xác minh: pytest 348 passed/1 skipped, tools/check_co4e.py sạch, không
file nào >400 dòng, domain/application không import PySide6, và so pixel
before/after (git worktree tại HEAD cũ) ra 0/1.125.000 pixel khác biệt.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
conversation application service, AtomicJsonFile fix) vào sau khi tách 6
widget UI Co4E (N3)
Đã kiểm trước khi merge: ui/co4e_tab.py và ui/routing_toggle.py đều bị 2
bên cùng đụng, nhưng ở vùng dòng khác nhau hoàn toàn (bên kia sửa
_apply_co4e_routing/RoutingToggle cho R03-T05, N3 chỉ đụng phần dựng
sidebar/canvas/chat) — không có xung đột logic thật.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Bốn mục trong Cài đặt tách thành widget riêng dưới presentation/settings/:
general_settings_widget.py ngôn ngữ, giao diện, khay, gợi ý
provider_settings_widget.py provider, base URL, key, model + 2 nút nền
parameter_settings_widget.py đính kèm, cấu trúc, giới hạn sandbox
routing_settings_widget.py Auto Model Routing
Mỗi widget tự dựng control, tự nạp giá trị, tự có apply_to(data). Dialog chỉ
còn lắp ráp và gọi apply_to lúc lưu — _save từ 34 dòng xuống còn phần khung.
Làm lưới an toàn trước khi bóc: tests/ui/test_settings_dialog_dac_ta.py, 7
bài đặc tả hành vi hiện tại (mục nào có mặt, nạp đúng giá trị gì, lưu ghi vào
đúng ô nào, đổi % sang phân lẻ, xoá cache sau lưu). Bóc xong cả 7 vẫn xanh,
và trong lúc bóc chúng đã đỏ đúng hai lần ở chỗ đáng đỏ.
Đây là repo chưa từng có test Qt nào — thêm tests/ui/conftest.py dựng
QApplication offscreen. Offscreen là bắt buộc chứ không phải cho nhanh: máy
dev là máy làm việc thật, test bật cửa sổ lên là nó nhảy ra che màn hình.
Dọn kèm:
* bỏ vòng "dựng vào layout rồi lại gỡ ra" của mục Chung, cùng widget cao 0px
làm mốc cuộn — không cần nữa khi mục đó tự là một widget
* bỏ _select_combo, _secret, _model_combo, _with_load và 4 hàm provider khác
đã chuyển vào widget (127 dòng)
* bỏ 5 import chết theo (Dict, QSizePolicy, PROVIDER_LABELS, SegmentedControl,
LANGUAGES)
Giữ cầu tương thích: self.routing_*, self.prov_*, self.attach_* … thành
property trỏ vào widget con, vì 5 checker trong tools/ đọc thẳng tên cũ. Bỏ
được khi tools/ chuyển sang đọc self._provider_page.
Hai điều KHÔNG làm, ghi lại để khỏi tưởng là quên:
1. Plan ghi 4 widget và có tên `connector`. Thực tế UI connector đã dời khỏi
Cài đặt từ trước (ghi chú ở settings_dialog.py:180 bản cũ), nên số mục thật
là 5, không phải 4, và không có mục nào tên connector. Bốn mục bóc ra là 4
mục có thật; mục Bảo mật sandbox để nguyên trong dialog lần này.
2. Còn ~108 dòng chết của MS365 (_refresh_ms365_status, _ms365_sign_in,
_show_ms365_device_code, _ms365_sign_out): đọc self.ms365_status,
self.ms365_signin_btn, self.ms365_signout_btn — ba thuộc tính KHÔNG BAO GIỜ
được gán, và không hàm nào có người gọi. Gọi vào là AttributeError. Chưa
xoá vì đó là quyết định của anh Nam, không phải việc kèm theo của T07.
437 test xanh. check_dialogs, check_no_hscroll, check_design_parity đều qua.
Kèm docs/refactor/tin-gui-team-hoa.md — tin báo Hoa về platform/ -> adapters/
và bản vá Windows của AtomicJsonFile.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Không xung đột — Lâm động vào ui/co4e_tab.py và presentation/co4e/,
tôi động vào ui/settings_dialog.py và presentation/settings/. Đúng như
quy tắc phân chia sở hữu đặt ra từ đầu.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Từ 21/08 tôi đã viết xong 7 file R02 với 46 test xanh, và báo là "xong R02".
Báo sai: code mới nằm song song, KHÔNG một dòng nào ngoài infrastructure/ và
tests/ gọi tới nó. App vẫn chạy nguyên trên config.py, 29 file dùng nó, và
khoá API của người dùng vẫn nằm plaintext trong config.json suốt 4 ngày.
Commit này mới là phần refactor thật.
Bù 21 thành viên còn thiếu (85 dòng)
------------------------------------
JsonConfigRepository có 18/34 thành viên công khai của AppConfig nên không
tráo được. Chép nguyên ngữ nghĩa 21 cái còn lại: load, ms365_*, ext_connectors,
connect_external, routing_mode_for, seeded_*, mcp_servers, teams, history,
structure, monitoring_visibility, model_label, ca_bundle... Giờ 40/34, không
thiếu gì. Không phải thiết kế mới — chừng nào 29 file còn gọi qua ctx.config
thì repository phải trả lời được đúng các câu hỏi cũ.
ROUTING_MODES lấy theo bản Delta (4 chế độ, có "fallback" từ R03-T03) chứ
không theo bản main cũ 3 chế độ. Chép bản cũ là routing "fallback" âm thầm rơi
về "off" sau khi Delta merge, không lỗi nào báo.
Composition Root (R08-T10, phần đầu)
-------------------------------------
presentation/shell/bootstrap.py: một chỗ duy nhất quyết định app dựng bằng
mảnh nào. app.py::run giờ gọi build_context() thay cho AppConfig.load().
Đây cũng là chỗ ráp kho bí mật vào; máy không có keyring thì secrets=None và
mọi thứ chạy như cũ.
Kiểm trên dữ liệu thật
----------------------
Chạy lên máy tôi, migration tự chạy đúng như thiết kế:
openai_compat 39 ký tự config.json -> Windows Credential Manager
ollama giá trị bù nhìn, để nguyên trong file, không đẩy vào kho
schema_version 1 -> 2
sao lưu config.json.v20260825-193206.bak
Sau khi bật lại app và để nó ghi cấu hình, config.json vẫn sạch: api_key rỗng,
không còn chuỗi nào có hình dạng khoá. scripts/audit_security.py sạch.
Tiêu chí nghiệm thu A của plan (dòng 244) — "0 lưu trữ plaintext API Key trong
JSON" — tới commit này mới thật sự đạt.
632 test xanh. check_dialogs, check_nav, check_design_parity đều qua.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
app.py 1356 -> 1293 dòng. presentation/shell/ có 3 file:
bootstrap.py Composition Root (đã vào ở commit trước)
tray_manager.py khay hệ thống + thông báo bong bóng
lifecycle_coordinator.py canh cửa sổ theo màn hình + tắt cho sạch
Vì sao tách khay: khay là thứ CÓ THỂ KHÔNG TỒN TẠI (một số môi trường Linux,
phiên RDP). Trước đây mỗi chỗ dùng phải tự nhớ kiểm `if self.tray is not None`
— có 6 chỗ như thế, và 3 chỗ còn phải tự bọc try/except quanh showMessage.
Gói lại thì chỗ gọi cứ gọi, không có khay thì không có gì xảy ra.
Vì sao tách vòng đời: hai việc trong đó không phải việc của giao diện. Canh
cửa sổ theo màn hình là số học thuần (anh Nam có hai màn khác độ phân giải và
khác tỉ lệ phóng — kéo qua lại là vùng làm việc đổi). Còn shutdown là thứ tự
dừng có ý nghĩa: bộ lập lịch trước để nó không kịp khởi động việc mới trong
lúc ta đang dừng việc cũ, rồi mới tới worker, rồi ngắt tiến trình MCP.
closeEvent/moveEvent/resizeEvent vẫn ở lớp cửa sổ vì Qt gọi thẳng vào đó,
nhưng phần quyết định đã chuyển đi. closeEvent từ 30 dòng còn 11.
Giữ self.tray thành property trỏ vào self._tray.icon — vài chỗ còn đọc tên cũ.
Đã lấy mốc trước khi bóc rồi so lại sau: 24/24 checker trong tools/ qua cả hai
lần. Đây là bộ đặc tả thật cho MainWindow (check_nav, check_rail_align,
check_layout_geometry, check_controls_alive... dựng cửa sổ thật offscreen trên
BẢN SAO của ~/.cowork_local, scheduler bị vô hiệu hoá). 632 test xanh.
CHƯA làm hết R08-T10: plan ghi tách thành main_window.py + tray_manager.py +
lifecycle_coordinator.py. Hai file sau đã xong, main_window.py thì chưa —
MainWindow vẫn nằm trong app.py và vẫn 1095 dòng. Đo lại thì khối lượng không
nằm ở ba cụm plan nêu mà ở hai cụm khác:
nav rail 18 method, ~340 dòng
topbar 8 method, ~157 dòng
__init__ 279 dòng
Hai cụm đó dính chặt vào state của cửa sổ, chuyển đi cần đổi giao diện giữa
chúng chứ không phải dời chỗ, nên tôi dừng ở đây thay vì làm nửa vời.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sót lại từ lần dời UI Connector sang Monitoring → Tools → Connector. Năm hàm:
_refresh_ms365_status 13 _show_ms365_device_code 52
_ms365_sign_in 34 _ms365_sign_out 4
_close_ms365_code_dialog 5
Chứng minh chết trước khi xoá, không xoá theo cảm tính:
* Dựng đồ thị lời gọi bằng ast: **mọi** lời gọi tới năm hàm này đều xuất phát
từ bên trong chính năm hàm đó. Không một đường vào nào từ ngoài cụm — cả
trong file lẫn toàn repo.
* Ba thuộc tính chúng đọc — ms365_status, ms365_signin_btn, ms365_signout_btn
— **chưa từng được gán ở đâu**. Gọi vào là AttributeError, không phải chạy sai
mà là sập.
* _ms365_workers chỉ được append bên trong _ms365_sign_in, nên chết theo.
Dọn kèm 6 import chỉ còn dòng import: AgentWorker, icon, EXT_CATEGORIES,
ExtConnectorEditDialog, AppContext, QTreeWidget.
Viết lại docstring đầu file — bản cũ vẫn mô tả file này chứa nhóm Connector
(CAD/CAE/MS365/Other), thứ đã không còn ở đây từ lâu.
settings_dialog.py: 407 -> 303 dòng. Cộng cả R08-T07 thì từ 727 xuống 303.
632 test xanh. check_dialogs, check_no_hscroll, check_design_parity,
check_orphans, check_probes_bite đều qua.
Ghi lại một phát hiện phụ, CHƯA xử lý: i18n.py có 28 khoá settings.ms365_*
mồ côi — 20 khoá đã không ai dùng từ trước lần dời connector, 8 khoá vừa mồ
côi theo commit này. Chỉ 2 khoá còn sống (ms365_local_connected,
ms365_local_none, dùng ở ui/connectors_panel.py). Xoá khoá dịch là đụng vào
dữ liệu ba ngôn ngữ ở file khác nên để anh Nam quyết riêng.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
config.py 623 -> 377 dòng (qua ngưỡng 400 của CASAN Check 2).
Class AppConfig 278 dòng giờ còn 30: mọi lối vào dẫn tới JsonConfigRepository.
Không xoá hẳn vì cái tên còn nằm ở 41 file — 23 checker trong tools/ và 18 file
test, trong đó có test của cả ba người. Sửa 41 chỗ trong một commit là đổi thứ
không cần đổi và làm review không đọc nổi. Giữ tên, đổi ruột.
Thêm JsonConfigRepository.from_data() cho dạng AppConfig(data=..., path=...) mà
13 file test đang dùng: dựng thẳng từ dict, không đọc đĩa, không chạy migration
trên dữ liệu test.
MỘT LỖI TÔI GÂY RA HÔM 25/08, HÔM NAY MỚI LỘ
---------------------------------------------
Lúc tráo R02 tôi có đối chiếu API và kết luận "đủ 34/34 thành viên, thay được".
Đối chiếu đó chỉ so TÊN, không so việc một property có setter hay không.
AppConfig cũ là dataclass nên `config.language = "vi"` chạy bình thường.
Repository để language là property chỉ đọc -> gán vào là AttributeError. Ba chỗ
trong app.py đang gán: đổi ngôn ngữ, đổi giao diện, đổi provider trên thanh bên.
Khó thấy vì cả ba nằm trong slot của Qt, mà Qt NUỐT ngoại lệ trong slot. Không
traceback, không thông báo — bấm đổi ngôn ngữ thì không có gì xảy ra. 709 test
đơn vị vẫn xanh suốt. Chỉ check_nav bắt được vì nó bấm thật vào combo rồi kiểm.
Thêm setter cho theme/language/active_provider, và tests/test_config_gan_duoc.py
đi ngược từ mã nguồn: quét cả repo tìm mọi chỗ `config.X = ...` rồi thử gán
thật. Đã kiểm ngược — bỏ setter đi thì 2 bài đỏ.
BẮC CẦU CHO 55 CONTROL MONITORING
----------------------------------
check_controls_alive so với mốc git 291a611 và đòi 55 control ov_* của Tổng
quan phải còn tới được. Sau khi Hiệp tách 8 tab, chúng về đúng tab/thẻ của mình
và rụng tiền tố -> 3 checker đỏ.
Control còn đủ, chỉ đổi chỗ ở. Bắc cầu bằng __getattr__ định tuyến theo tiền tố
(ov_perm_ -> permissions_card, ov_sbx_ -> sandbox_card, ov_price_/ov_pricing_ ->
pricing_panel, còn lại -> overview_tab), cộng 3 hộp nhóm mà bản thân widget con
chính là hộp đó.
Định tuyến theo tiền tố chứ không dò mờ: overview_tab và permissions_card đều
có network_lbl — một cái là mức dùng mạng, một cái là quyền truy cập mạng. Bản
dò mờ đầu tiên tôi viết vớ nhầm cái đầu tiên tìm thấy.
714 test xanh. 24/24 checker qua (3 cái đã đỏ từ trước khi tôi bắt đầu, do phần
monitoring, nay xanh lại). CASAN Check 1 sạch.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Đây là deliverable còn thiếu duy nhất trong 17 task của Gamma.
app.py 128 chỉ còn điểm vào chương trình
presentation/shell/
main_window.py 362 __init__ + vòng đời cửa sổ
nav_rail.py 385 dựng rail + cây điều hướng + thu gọn
top_bar.py 234 thanh trên + tài khoản + đáy rail
session_events.py 104 lịch sử, thông báo task xong
page_registry.py 82 4 màn chính, dựng lười, _goto
rail_project.py 132 bộ chọn project + RECENTS
lifecycle_coordinator.py 110 canh màn hình + tắt sạch
tray_manager.py 76 khay hệ thống
toast.py 40 thông báo góc trên trái
bootstrap.py 42 Composition Root
branding.py 26 ASSETS + app_icon
rail_metrics.py 37 kích thước rail + cách vẽ hàng
Mọi file dưới 400 dòng. Đây là ngưỡng CASAN Check 2.
NÓI THẲNG VỀ CÁCH TÁCH: sáu file trong đó là MIXIN, không phải widget rời.
Cả loạt phương thức đọc/ghi state của cửa sổ (self._page_widgets, self.workspace,
self.splitter...). Biến thành đối tượng cộng tác thì phải viết lại từng chỗ
self.X thành self.window.X — gần 800 dòng sửa chỉ để đổi cách gọi, rủi ro cao
mà không đổi hành vi. Mixin cho đúng thứ đang cần: mỗi mảng một file, ai sửa
rail thì mở file rail. Chuyển thành widget thật khi có cửa sổ thứ hai cần dùng
lại — hiện chưa có.
Giữ đường vào cũ: MainWindow, app_icon, _NAV_*, _Toast vẫn import được từ
cowork_local.app, nên 24 checker trong tools/ không phải sửa.
BA LỖI TỰ GÂY TRONG LÚC TÁCH, ĐỀU DO CHECKER BẮT
-------------------------------------------------
1. 12 import lazy nằm trong thân hàm bị thụt lề nên regex đổi mức tương đối
của tôi bỏ sót -> ModuleNotFoundError khi bấm vào rail.
2. Bộ dò import thiếu của tôi tính cả import cục bộ trong hàm KHÁC, nên tưởng
QHBoxLayout đã có -> 17 checker đỏ. Bỏ cách dò, cấp thẳng khối import đầy
đủ rồi cắt phần không dùng.
3. Hằng số ASSETS và _NAV_* nằm ở khối tôi không mang theo -> NameError.
Cả ba đều là lỗi im lặng với bộ test đơn vị (714 vẫn xanh suốt) và chỉ lộ khi
dựng cửa sổ thật. Đó chính là lý do bộ checker trong tools/ tồn tại.
Cập nhật 2 đích đột biến của check_probes_bite: mã nó cần sửa đã dời khỏi
app.py sang rail_project.py và nav_rail.py.
714 test xanh. 24/24 checker qua.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
File to nhất còn lại của Gamma. Lâm bàn giao ở 1.885 dòng với 100 method
trong một lớp; chia theo bảy mối quan tâm:
co4e_runs.py 364 chạy flow, 3 chế độ, bảng lịch sử lượt chạy
co4e_chat.py 343 khung chat + đếm token + định tuyến riêng
co4e_layout.py 308 ba khung, bảng cấu hình, bố cục màn hẹp
co4e_sidebar.py 251 thư viện workflow/agent/skill, 4 mục gập
co4e_flow_tabs.py 180 dải tab các flow đang mở
co4e_workflow_crud.py 154 tạo/sửa/xoá/nhân bản workflow
co4e_agents.py 51 agent và skill dùng trong flow
ui/co4e_tab.py 389 __init__, set_project, thư mục output
Mọi file dưới 400 dòng.
MỘT LỖI SUÝT LÀM HỎNG FILE: bản đầu tôi cắt method theo m.lineno, mà lineno
trỏ vào dòng `def`, không tính dòng `@...` phía trên. Decorator bị bỏ lại
thành mồ côi ngay trên một hằng số lớp -> file hỏng cú pháp. Bắt được vì
script tự parse lại sau mỗi lần cắt; nếu chỉ cắt rồi ghi thì đã đẩy lên một
file không import nổi.
Ba vòng sửa mức import tương đối: co4e_tab.py nằm ở ui/ (1 cấp), file mới ở
presentation/co4e/ (2 cấp). Còn co4e_canvas / co4e_config_panel /
co4e_agent_dialog thì VẪN ở ui/, nên `.co4e_canvas` phải thành
`...ui.co4e_canvas` chứ không phải `.co4e_canvas` cùng thư mục.
714 test xanh — trong đó có ~4.000 dòng test đặc tả Lâm viết cho đúng vùng
này, nên việc tách được soi khá kỹ. check_co4e, check_controls_alive,
check_layout_geometry, check_probes_bite đều qua.
Cập nhật đích đột biến thứ ba của check_probes_bite: dải tab flow nay ở
presentation/co4e/co4e_layout.py.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Ba file dữ liệu cuối cùng của Gamma còn trên ngưỡng CASAN Check 2.
i18n.py 3075 -> 94
Dict STRINGS 3.000 dòng cắt thành 10 cụm theo đúng mốc phân đoạn có sẵn
trong file (mỗi mốc là một màn/hộp thoại), cụm nào quá dài thì cắt tiếp ở
ranh giới khoá. i18n.py giờ chỉ gộp lại và giữ 4 hàm set_language/
get_language/tr/on_language_changed.
Kiểm bằng cách so với bản gốc lấy từ git: 1437 mục / 1431 khoá duy nhất
(bản gốc vốn có 6 khoá lặp), sau khi chia vẫn 1431, KHÔNG thiếu khoá nào,
KHÔNG thừa khoá nào, KHÔNG giá trị nào lệch. Thứ tự gộp giữ nguyên nên
quy tắc "khoá trùng thì bản sau thắng" không đổi.
theme.py 907 -> 130
theme_palettes.py 328 hai bảng màu Tối/Sáng + lớp Palette
theme_qss.py 198 nửa vỏ (reset + shell)
theme_qss_controls.py 307 nửa điều khiển (nút, ô nhập, tab, badge)
Khuôn QSS 470 dòng cắt đôi đúng mốc `/* ---- surfaces */` của chính nó.
Đã đối chiếu: stylesheet('dark') ra đúng 24762 ký tự y như trước — khớp
từng byte, không phải "trông có vẻ giống".
core/usage_tracker.py 536 -> 307
usage_cost.py 101 bảng giá, quy đổi token sang tiền, định dạng
usage_periods.py 144 gộp theo ngày/tuần/tháng/quý, chuỗi vẽ biểu đồ
usage_ai_report.py 56 dựng câu nhắc cho AI phân tích
HAI LẦN TỰ CẮT HỎNG, ĐỀU CÙNG MỘT GỐC
--------------------------------------
1. Cắt theo m.lineno mà quên dòng @decorator phía trên -> @dataclass của
Palette bị bỏ lại mồ côi, "Palette() takes no arguments".
2. Đọc số dòng từ AST GỐC trong khi danh sách dòng đã bị cắt -> lần bóc thứ
hai dùng toạ độ cũ và cắt vào giữa một chữ ký hàm.
Cả hai lộ ngay vì mỗi script tự parse lại sau khi ghi. Bài học đã áp vào cả
ba lần chia: parse lại sau mỗi lần cắt, và luôn tính cả decorator.
KẾT QUẢ CASAN CHECK 2
---------------------
Nam 0 file vượt 400 (trước: 4, tổng 5.898 dòng)
Hiệp 0 (trước: 1)
Lâm 0 (trước: 1)
file mới 0 (61 file dưới presentation/ application/
domain/ infrastructure/ — chưa cái nào vượt)
Gamma sạch. 23 file còn vượt đều thuộc team khác (chat_panel.py 1802,
folder_tab.py 1589, structure_graph_view.py 1034...) — cần báo lên sớm chứ
đừng để tới hạn 30/08 mới lộ.
714 test xanh. 24/24 checker qua. CASAN Check 1 sạch.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Soát lại plan.md thì thấy CASAN là NĂM tiêu chí C-A-S-A-N, không phải ba. Tiêu
chí A có hai vế, tôi mới đạt vế đầu:
vế 1 0 API key plaintext trong JSON -> đã đạt từ 25/08
vế 2 MỌI thao tác ghi tệp đi qua AtomicJsonFile -> CHƯA
Toàn repo còn 15 chỗ ghi JSON thẳng. Bốn trong đó là của Gamma (vùng Co4E):
core/co4e.py:236 lưu workflow ghi thẳng, không nguyên tử gì cả
core/co4e.py:310 lưu agent ghi thẳng
core/co4e_run_manager.py:156 tmp + replace tự viết
application/workflows/co4e_workflow_service.py:178 tmp + replace tự viết
Hai chỗ đầu nguy hơn: tắt máy giữa lúc lưu là mất luôn workflow hoặc agent.
Hai chỗ sau nhìn thì có vẻ ổn vì đã tmp + replace, nhưng thiếu hai thứ:
* không fsync — dữ liệu có thể còn nằm trong bộ đệm ổ đĩa khi mất điện, nên
"nguyên tử" chỉ đúng với crash tiến trình, không đúng với mất điện;
* dùng thẳng Path.replace, đúng chỗ dính PermissionError [WinError 5] mà tôi
vá hôm 25/08 — Defender giữ handle file vừa tạo. Tần suất đo được khoảng
1/140 lần lưu, nhân với số lần lưu lịch sử chạy flow.
11 chỗ còn lại thuộc team khác (accounts, admin_agents, custom_agents, flows,
groups, history, projects, skills, tasks). Không đụng vào; cần báo lên vì
tiêu chí A là tiêu chí TOÀN DỰ ÁN, Gamma sạch không cứu được cổng.
Đã kiểm application/ vẫn không kéo PySide6 vào sau khi thêm import mới
(tiêu chí C). 714 test xanh, 24/24 checker qua.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Team Hoa, EPIC R07 (Scheduling & Workflow Runtime) - Team Hoa scope only
(R07-T01 -> T05; R07-T06 Co4EWorkflowService is Team Nam's).
- R07-T01: infrastructure/persistence/json/task_repository_impl.py wraps
core/tasks.py's CRUD; core/tasks.py::save_task now writes through
atomic_write.write_json (same durability fix as R06-T02, save_task was
still doing a plain write_text).
- R07-T02: domain/tasks/schedule_calculator.py::ScheduleCalculator - the
cron/interval/daily/weekly/monthly due-time math extracted from
core/tasks.py, pure Python with is_holiday/make_cron injected so domain/
never imports core (ADR-001 I2). core/tasks.py keeps its old function
names as thin wrappers so every existing caller is unchanged. This was
previously untested; now has its own unit suite.
- R07-T03: infrastructure/qt/qt_scheduler_clock.py::QtSchedulerClock wraps
the QTimer TaskScheduler used to own directly, injected via a new
`clock=` constructor param (defaults to a real one). Originally planned
at platform/qt/... ; moved after confirming that name shadows the
stdlib platform module (used by core/windows_sandbox_vm.py,
core/appcontainer_sandbox.py) whenever the repo root is on sys.path.
tests/fakes/fake_clock.py lets scheduler dispatch be tested tick-by-tick
with no Qt event loop.
- R07-T04: application/scheduling/task_application_service.py centralizes
run_now/duplicate/pause/delete/bulk_delete and the Kanban drag-drop
business rules (move_to_status), currently only reachable by driving
the real ui/schedule_task_tab.py widget.
- R07-T05: application/scheduling/ai_task_planner_service.py wraps
core/ai_task_planner.py::plan_tasks and core/task_import.py::import_tasks
as a seam, plus the attachment-stamping step that used to only exist
inside the AI-create dialog's worker closure.
pytest: 328 pass (same 4 pre-existing failures as the R05/R06 baseline,
unrelated to this work - see docs/refactor/BaoCao_TeamHoa_R05_R06.md).
scripts/check_imports.py: PASS.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Team Hoa, EPIC R08 (UI/Application Separation) - Team Hoa scope only
(R08-T11 -> T14; R08-T01->T10 belong to Team Duy/Team Nam).
- R08-T11: ui/schedule_task_tab.py (795 lines) -> presentation/scheduling/
{kanban_board_widget,calendar_view_widget,ai_task_creator_dialog,
ai_task_import_dialog,run_history_dialog}.py + schedule_task_tab.py
shell. Kanban CRUD/drag-drop now goes through
application/scheduling/task_application_service.py (R07-T04) instead of
~30 lines of inline if/elif per drag target.
- R08-T12: ui/folder_tab.py (1587 lines, the largest of the four) ->
presentation/folder/{workspace_file_tree,document_preview_manager,
code_editor,office_document_renderer,ai_file_editor_dialog,
ai_edit_model_resolver,ai_edit_pipeline}.py + folder_tab.py shell.
Closes the R06-T05 loop: FileWorkspaceService existed since R06 with
zero production call sites (confirmed by grep); every plain-text write
(save/create/write_content) now goes through it, gaining path
containment and a Python-syntax warning the original code never had.
Pure helpers (_read_text, _is_probably_text, _pptx_available,
_split_code_block, _parse_ai_output) moved to
application/workspaces/{file_preview_helpers,ai_edit_output}.py.
- R08-T13: ui/dashboard_tab.py (437 lines) -> presentation/dashboard/
{token_usage_card_widget,usage_chart_widget,habits_widget}.py +
dashboard_tab.py shell, backed by a new
application/monitoring/dashboard_query_service.py (pricing/period/
summary queries the three widgets used to each recompute separately).
Directory-ownership note left in the checklist for Team Nam.
- R08-T14: ui/structure_graph_view.py (1035 lines) ->
presentation/graph/{graph_scene_items,graph_renderer,
graph_messages_view,graph_qa_widget}.py + structure_graph_view.py
shell. Extraction helpers (_pdf_to_markdown, _extract_file_contents)
moved to application/workspaces/graph_index_service.py (pure Python).
Renderer and Q&A panel talk only through signals
(node_selected/graph_rendered/raw_json_ready/project_changed) - neither
imports the other.
- presentation/shared/web_engine_support.py: HAS_WEB_ENGINE, previously
duplicated (folder_tab imported it FROM structure_graph_view.py) - now
one shared flag instead of one screen importing another screen's module.
All four old ui/*.py files deleted; app.py and ui/workspace_tab.py updated
to the new import paths (each god-file only had 1-2 real construction
sites, so import sites were updated directly rather than kept as a
strangler-fig shim - unlike core/tools.py at R05, which had dozens).
pytest: 377 pass (+94 vs the R07 baseline of 328; same 4 pre-existing
failures as the R05/R06 baseline, unrelated to this work).
scripts/check_imports.py: PASS. python -c "import cowork_local.app": OK.
Every new file < 400 lines (largest: graph_renderer.py, 391).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Replaces BaoCao_TeamHoa_R05_R06.md and BaoCao_TeamHoa_R07_R08.md with a
single BaoCao_TeamHoa_R05_R08.md covering all 4 EPICs (19/19 tasks) in
Team Hoa's scope, and updates the checklist's report links accordingly.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
token_usage_card_widget.py 93 5 thẻ số liệu + thẻ Ngân sách
usage_chart_widget.py 119 biểu đồ tuần/tháng/năm + đường so sánh
habits_widget.py 171 thói quen dùng token + nhận xét của AI
Ba widget THẬT, không phải mixin — khác với shell và Co4E, ba mảng này tách
bạch trên màn hình và không đọc state của nhau. Giao tiếp bằng signal:
budget_applied, filter_changed, status_message.
Điểm cần biết: các nút lật khoảng và hai ô chọn thuộc về UsageChartWidget
nhưng được Dashboard nhấc lên hàng điều khiển ở trên. Chúng là control của
biểu đồ, chỉ hiển thị ở chỗ khác.
Giữ 18 cầu tương thích cho tên cũ vì check_dashboard, check_design_parity và
check_controls_alive đọc thẳng self.card_total, self._chart_period_lbl...
756 test xanh. check_dashboard, check_design_parity, check_controls_alive qua.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
presentation/graph/
structure_graph_view.py 325 lớp chính + dựng giao diện
graph_qa_widget.py 322 hỏi-đáp trên đồ thị (_ask 119 dòng)
graph_render.py 226 quét, vẽ Qt + D3, xuất ảnh
graph_scene.py 138 node, cạnh, khung nhìn — thuần đồ hoạ
graph_project.py 109 chọn project, đổi tab xem
graph_web.py 38 cờ có dùng được QtWebEngine không
ui/structure_graph_view.py 11 vỏ chuyển tiếp, giữ đường import cũ
BA LẦN CẮT HỎNG, ĐỀU LÀ TÊN CẤP MODULE BỊ BỎ LẠI
------------------------------------------------
_HAS_WEB, QWebEngineView, QWebChannel, _Bridge, _Edge, _Node — tất cả định
nghĩa ở file gốc, dùng ở file mới, nên NameError ngay lúc chạy. Bộ test đơn
vị KHÔNG bắt được cái nào: 756 bài vẫn xanh suốt ba lần. Chỉ
check_graphrag_rescan bắt, vì nó gọi prewarm() thật rồi chờ đồ thị dựng xong.
Sau lần thứ ba tôi bỏ cách đuổi từng lỗi và viết bộ dò tên chưa định nghĩa có
tính đến phạm vi hàm (tham số, biến cục bộ, except-as, comprehension). Nó
tìm ra nốt _fmt_plan và _qcolor còn thiếu ở hai file Co4E đã tách hôm trước —
hai quả mìn chưa nổ.
_HAS_WEB tách hẳn ra graph_web.py: cả structure_graph_view.py lẫn
graph_render.py đều phải hỏi, để ở một trong hai là vòng import.
756 test xanh. 24/24 checker qua.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
presentation/scheduling/
calendar_view_widget.py 231 lịch tháng (chuyển từ ui/calendar_view.py)
ai_task_creator_dialog.py 208 tạo task bằng AI
task_actions.py 189 thêm/sửa/chạy/xoá/xem log một task
kanban_board_widget.py 98 cột Kanban + vùng thả file
run_history_dialog.py 82 lịch sử các lượt chạy
ai_task_import_dialog.py 81 nhập task từ file
ui/schedule_task_tab.py 297 dựng bảng + đổi chế độ xem
ui/calendar_view.py 10 vỏ chuyển tiếp
Plan ghi 4 file; thực tế cần 6. Hai file thêm là run_history_dialog.py và
task_actions.py — không tách thì schedule_task_tab.py còn 517 dòng, vẫn vượt
ngưỡng 400.
ai_task_import_dialog.py làm mixin chứ không phải hộp thoại rời: plan gọi nó
là dialog, nhưng thực tế nó là TAB THỨ HAI của cùng hộp thoại tạo task, dùng
chung phần xem trước và nút Xác nhận. Tách hẳn thì phải nhân đôi cả hai.
LẠI LỖI DECORATOR: script này tôi quên dùng bản có tính dòng @, nên một
@staticmethod bị bỏ lại mồ côi -> IndentationError. Đây là lần thứ tư cùng
một lỗi. Đã thêm bước dọn decorator mồ côi vào script.
756 test xanh. 16 checker chạy đều qua.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
presentation/folder/
ai_edit_runner.py 325 một lượt AI sửa file, từ gửi tới xem trước
document_preview_manager.py 317 PDF/Word/Excel/PowerPoint/ảnh/HTML/mã
ai_file_editor_dialog.py 317 dựng panel AI + chọn model
code_editor.py 183 ô soạn mã, đánh số dòng, tô cú pháp
ai_output_writer.py 140 phần DUY NHẤT chạm vào file người dùng
image_model_picker.py 115 dò model sinh ảnh trên mọi provider
file_helpers.py 112 nhận dạng loại file + ngưỡng
workspace_file_tree.py 38 cây thư mục
ui/folder_tab.py 305 lắp ráp + retranslate
Plan ghi 3 file; khối lượng thật cần 8. Hai file tôi thêm ngoài dự kiến vì
đọc kỹ thì chúng là ranh giới thật:
* ai_output_writer.py — tách ra vì đây là phần duy nhất THẬT SỰ ghi đè file
của người dùng. Mọi thứ trước nó chỉ dựng bản xem trước. Ranh giới đó đáng
nhìn thấy trong cấu trúc thư mục.
* image_model_picker.py — chỗ duy nhất trong màn Thư mục biết tới nhiều
provider cùng lúc (nó gợi ý được model sinh ảnh của provider KHÁC cái đang
chọn).
Gom mọi hằng nhận dạng loại file (_IMAGE_SUFFIXES, _HAS_PDF, _MAX_EDIT_BYTES…)
về file_helpers.py: cả tám file trong gói đều hỏi tới, để rải ra thì thêm một
đuôi file phải sửa vài chỗ.
LẠI IMPORT LAZY THỤT LỀ: regex đổi mức tương đối của tôi chỉ khớp đầu dòng
nên bỏ sót import nằm trong thân hàm — 3 checker đỏ. Lần này tôi sửa một lượt
cho CẢ cây presentation/ thay vì riêng thư mục vừa tách; nó tìm ra thêm 3 file
ở scheduling cũng đang sai mà chưa nổ.
756 test xanh. 24/24 checker qua.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
presentation/chat/
chat_history_widget.py 348 T01 mạch hội thoại (từ ui/chat_view.py)
chat_bubble_style.py 202 T01 cách vẽ bong bóng, diff, đường thời gian
composer_widget.py 364 T02 thanh công cụ quanh ô nhập
chat_input_box.py 328 T02 ô nhập: Ctrl+Enter, dán ảnh, popup /skill
attachment_picker.py 215 T03 đọc tệp đính kèm + chặn theo chính sách
chat_output_panel.py 186 T05 theo dõi thư mục output, hiện tệp mới
chat_turn_runner.py 281 T06 chạy một lượt
chat_event_stream.py 228 T06 nhận sự kiện phát về từ luồng nền
chat_session_store.py 413 T06 lưu/nạp phiên, đếm token, nối lại lượt
chat_agents.py 246 T06 chọn agent, skill, định tuyến model
chat_panel_layout.py 148 T06 bố cục hai cột
chat_helpers.py 53 T06 hàm và bảng tra dùng chung
ui/chat_panel.py 345 __init__ + trạng thái
ui/chat_view.py 10 vỏ chuyển tiếp
ui/composer.py 11 vỏ chuyển tiếp
R08-T04 KHÔNG LÀM ĐƯỢC: plan đòi audio_recorder_widget.py, nhưng trong repo
KHÔNG CÓ chức năng ghi âm nào — grep 'audio|record|voice|micro' toàn ui/ chỉ
ra chữ 'record' trong nghĩa 'ghi lại transcript'. Không có gì để tách, và tôi
không dựng một widget mới nhân danh refactor. Giống hệt trường hợp
connector_settings_widget.py ở T07.
_start_turn (144 dòng) và _on_event (127) để nguyên có chủ ý: cái đầu dựng
trọn ngữ cảnh một lượt rồi giao cho luồng nền, cái sau phân nhánh theo loại sự
kiện. Cắt nhỏ thì phải chuyền hàng chục biến trạng thái qua lại, đọc khó hơn.
Hai lỗi tự gây, cả hai đều do script:
* regex bỏ import cũ chỉ cắt DÒNG ĐẦU của một import nhiều dòng, để lại phần
đuôi mồ côi -> IndentationError.
* _build_layout dùng biến 'root' vốn cục bộ trong __init__. Bộ test bắt được
cái này (2 bài integration đỏ), không phải checker — vì nó là lỗi dựng
widget, không phải lỗi hình học.
756 test xanh. 24/24 checker qua.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
chat_live_turns.py (90 dòng) là phần tinh tế nhất của khung chat: người dùng
mở phiên khác rồi quay lại trong khi lượt cũ vẫn đang chạy. Phải nối vào đúng
luồng đó và đúng danh sách tin nhắn đang sống, chứ không đọc bản trên đĩa (đã
cũ) hay khởi động lại. Sai thì hoặc mất phần agent viết lúc mình vắng mặt,
hoặc hai bên cùng ghi vào một file.
Giờ Gamma không còn file production nào vượt 400 dòng.
756 test xanh.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Soát lại từng dòng plan thì thấy tôi báo R08 xong hơi sớm. Ba chỗ thiếu thật:
T06 ChatPanel vẫn ở ui/, plan đòi presentation/chat/chat_panel.py
T08 agents_admin_tab.py (498) và tools_admin_tab.py (245) vẫn ở ui/
presentation/chat/chat_panel.py 346
presentation/monitoring/tabs/agents_admin_tab.py 383
presentation/monitoring/tabs/agent_edit_dialog.py 143
presentation/monitoring/tabs/tools_admin_tab.py 245
ui/chat_panel.py / agents_admin_tab.py / tools_admin_tab.py ~10 mỗi cái
agents_admin_tab.py 498 dòng nên tách thêm agent_edit_dialog.py: bảng danh
sách và hộp thoại sửa là hai việc, và hộp thoại còn tự đi hỏi provider xem có
model nào — thứ bảng không cần biết.
BA CHỖ CÒN LẠI KHÔNG PHẢI THIẾU, đã kiểm từng cái:
* audio_recorder_widget.py (T04) — repo KHÔNG có chức năng ghi âm nào.
* connector_settings_widget.py (T07) — UI Connector đã dời khỏi Cài đặt.
* sandbox_status_tab.py / mcp_history_tab.py (T08) — Hiệp đặt tên sandbox_tab
và mcp_tab, nội dung đủ.
R08: 14/14 task, 0 file thiếu thật sự.
756 test xanh. 24/24 checker qua.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Resolve 3 file conflict:
- docs/refactor/Refactoring_Checklist.md: giữ nội dung incoming (phía HEAD
trống ở đoạn conflict).
- tests/integration/test_routing_surfaces.py: khôi phục từ incoming (bị mất
ở merge trước đó), điều chỉnh lại cho khớp API hiện tại của
RoutingApplicationService (resolve()/RouteEvaluation/mode_resolver thay vì
route_turn()/mode_reader cũ), bỏ 2 test pin một lớp RoutingDecision không
còn tồn tại trên nhánh này.
- ui/folder_tab.py: chấp nhận xoá (deleted by them) — đã được thay thế hoàn
toàn bởi presentation/folder/* (R08-T12), không còn nơi nào import module
cũ.
Sửa thêm 2 chỗ lệch API bị auto-merge không báo conflict (phát hiện khi chạy
lại test):
- presentation/folder/ai_edit_model_resolver.py + ai_file_editor_dialog.py:
AiEditModelResolver.apply_routing() gọi route_turn() đã bị xoá khỏi
RoutingApplicationService — chuyển sang build_routing_application_service()
.resolve(RoutingRequest(...)) giống chat_panel.py/co4e_chat.py; sửa luôn
chữ ký _confirm_routing_switch nhận thêm timeout cho khớp contract confirm
mới.
- config.py: import JsonConfigRepository ở đầu file gây circular import với
core/tasks.py (cần CONFIG_DIR) qua chuỗi mới
infrastructure/persistence/json/task_repository_impl.py (R07). Dời import
xuống ngay trước chỗ dùng đầu tiên.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Đồng nghiệp đã push thêm 10 commit lên nhánh trong lúc đang xử lý merge
trước đó (R08-T01..T06 chat_panel.py split, R08 folder/dashboard/graph/
scheduling hoàn thiện, R10 CI Quality Gates + Contributor Recipes + E2E
smoke test). Resolve conflict:
- application/monitoring/__init__.py, domain/tasks/__init__.py,
infrastructure/persistence/json/__init__.py: chỉ khác docstring — giữ bản
HEAD (đầy đủ ngữ cảnh EPIC hơn), hợp nhất __all__ khi cần
(MonitoringQueryService).
- tests/fakes/__init__.py: hợp nhất __getattr__ để lazy-load cả
FakeToolExecutor lẫn ToolInvocation (bản HEAD thiếu ToolInvocation), bỏ
entry "FakeClock" bị lặp trong __all__.
- tests/integration/test_routing_surfaces.py (deleted by them): khôi phục
lại bản đã sửa ở lần merge trước — verify lại: API routing
(RoutingApplicationService.resolve/_apply_routing/_apply_co4e_routing)
không đổi sau khi chat_panel.py chuyển sang presentation/chat/*, 9/9 test
vẫn pass trên code đã merge.
Ghi chú (không sửa, ngoài phạm vi merge): tests/fakes/__init__.py trên nhánh
remote export "ToolInvocation" từ fake_tool_executor.py nhưng class này đã
bị xoá nhầm từ commit chung 10739f1 (breakdown folder tree epic R01) — hiện
là dead code, không ai import, nhưng sẽ raise ImportError nếu có test nào
sau này thử dùng.
Đã chạy pytest tests/: 793 passed (không phát sinh fail mới so với lần
merge trước — 8 fail còn lại đều do môi trường sandbox: thiếu package
keyring, và tên thư mục checkout "cowork-local" thay vì "cowork_local"
khiến vài test spawn-subprocess không import được package).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Hai bản tách song song của cùng một god-file cùng được giữ lại sau một lần
merge. Bản chết không ai import, và hai file trong đó còn không import nổi:
`graph_render.py` lấy `GraphQaMixin` không tồn tại, `task_actions.py` lấy
`ui.calendar_view` đã bị xoá.
Kèm theo 5 gói chỉ có `__init__.py` với docstring hứa những module chưa bao
giờ được tạo. Hai trong số đó (`adapters/qt/`, `infrastructure/platform/qt/`)
là vị trí đã bị bác bỏ có ghi lý do — `QtSchedulerClock` nằm ở
`infrastructure/qt/`, và lý do vì sao không đặt ở `platform/` vẫn còn nguyên
trong `infrastructure/qt/__init__.py`.
Không cổng nào bắt được đám này: file không ai import vẫn đúng chiều phụ
thuộc, vẫn sạch credential, vẫn dưới 400 dòng. Cổng O ở commit sau đi tìm
đúng khoảng trống đó.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Gate O — module production phải có ít nhất một nơi import
---------------------------------------------------------
Ba cổng đang có đều không bắt được mã chết, đúng như 1.400 dòng ở commit
trước đã chứng minh. Gate O dựng đồ thị import bằng AST từ
`__init__`/`__main__`/`app`, theo cả import muộn trong thân hàm.
Hai ngoại lệ tự động để `ALLOWLIST` không phải chép lại cùng một lý do nhiều
lần: `__init__.py` của gói mà mọi thành viên đều dormant, và module chỉ được
chính mã dormant đã miễn trừ import.
Cổng cũng đếm tuổi 9 seam chưa nối dây (nhãn `SEAM · dựng <ngày>`) và nhắc
khi quá 30 ngày. Chỉ [WARN], không làm CI đỏ: để nó đỏ thì CI sẽ đỏ vào một
buổi sáng mà không ai sửa gì, và cách nhanh nhất để xanh lại là sửa ngày.
Cổng LOC — quét 366 file thay vì 191
------------------------------------
`DEFAULT_TARGET_DIRS` chỉ có 4 gói Clean Architecture, nên một file 944 dòng
trong `ui/` vẫn qua cổng. Nay quét cả `ui/`, `core/`, `providers/`,
`security/`, `mcp_servers/` và các module ở thư mục gốc.
18 file đã dài hơn 400 dòng từ trước nằm trong `LEGACY_ALLOWANCE` — bánh cóc
chỉ quay một chiều, và nó đo DÒNG MÃ chứ không đo dòng vật lý. Bánh cóc chỉ
hỏi một câu, "file này có đang để thêm việc vào không?", mà viết thêm một
docstring thì không. Đếm dòng vật lý ở đó biến cổng thành thứ phạt người viết
tài liệu, và cách dễ nhất để làm nó xanh lại sẽ là xoá bớt chú thích. Trần
400 vẫn đếm dòng vật lý — đó là hợp đồng đã chốt của cổng S.
CI
--
Ghim tên thư mục checkout là `cowork_local`: nhiều test characterization sinh
tiến trình con `python -c "from cowork_local... import ..."`, mà tiến trình
con chỉ import được khi trên sys.path có thư mục mang đúng tên gói. Checkout
vào thư mục tên khác làm 73 test đỏ vì lý do không liên quan tới mã.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`tools/` không đổi một byte nào giữa hai bản, nhưng 5 checker vẫn chết vì
chúng tìm control bằng `getattr(root, "ten")` trên đúng widget cũ — mà R08 đã
dời control xuống widget con.
Thêm ba helper dùng chung vào `capture_screens.py`:
* `_own_member` — tên do app khai trên widget, không phải thừa kế từ Qt
* `owner_of` — widget thật sự đang giữ tên đó, duyệt theo bề rộng
* `control` — lấy control dù nó nằm ở cấp nào
`check_controls_alive` từ "MẤT 24 control" về 0, kèm liệt kê 22 control đã
đổi chỗ và 2 cái đổi tên. `check_probes_bite` từ 1/4 lên 6/6 phép cấy lỗi đều
bị bắt — phép cấy thứ hai trỏ vào `ui/schedule_task_tab.py` đã bị xoá, nay
trỏ vào `presentation/scheduling/kanban_board_widget.py`.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Trình chạy
----------
Cả hai lệnh trong README đều không chạy được từ một thư mục checkout tên
khác `cowork_local`:
python -m cowork_local -> No module named cowork_local
python __main__.py -> ModuleNotFoundError: No module named 'cowork_local'
Không sửa được bằng mẹo sys.path, vì `state.py` khởi động máy chủ MCP MS365
bằng tiến trình con `python -m cowork_local.mcp_servers.ms365_server` — tiến
trình con cũng phải import được. Hai script tạo một junction ở
`%LOCALAPPDATA%\CoworkLocal\launcher` thay vì bắt người dùng đổi tên thư mục
làm việc.
Môi trường ảo đặt ở `%LOCALAPPDATA%\CoworkLocal\venv`, cố ý KHÔNG đặt trong
repo: các cổng chất lượng quét toàn bộ cây thư mục chứ không đọc
`.gitignore`, nên một `.venv` ở đây sẽ biến vài nghìn module thư viện thành
"mã production không ai import" và làm Gate O đỏ.
requirements.txt
----------------
Chạy thử `run.bat` trên một profile trắng thì app chết ngay lúc mở:
presentation/folder/code_editor.py:41
ModuleNotFoundError: No module named 'pygments'
Quét toàn bộ import bên thứ ba thì thiếu 8 thư viện, trong đó `pygments` và
`pydantic` là bắt buộc — import không có try/except, nên triệu chứng không
phải "tính năng đó không chạy" mà là app không mở được. Nghĩa là cài đúng
theo requirements.txt xong app vẫn hỏng.
Đã tách rõ nhóm bắt buộc / tuỳ chọn kèm lý do từng dòng.
`opendataloader-pdf` để nguyên dạng chú thích vì code tự cài khi cần qua
`core/deps.py::ensure_module`.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`theme_qss.py` đặt `font-weight: 600` cho nút đang chọn, nhưng `QPushButton`
tính `sizeHint()` theo phông thường. Chữ đậm rộng hơn — nên đúng lúc một mục
được chọn thì nó không còn đủ chỗ và Qt cắt bớt chữ.
Đo được trước khi vá:
Tiếng Việt 85px cần 87px thiếu 2px
English 67px cần 69px thiếu 2px
Tự động (theo hệ thống) 170px cần 177px thiếu 7px
日本語 50px cần 50px —
Tiếng Việt lộ rõ nhất vì nó vừa là nhãn dài nhất trong dải ngôn ngữ, vừa có
dấu, và với người dùng tiếng Việt thì nó LUÔN là mục đang được chọn, tức luôn
là mục bị in đậm. Chữ Nhật không dính vì bề rộng glyph CJK không đổi theo độ
đậm.
Cách vá: chừa sẵn bề rộng cho chữ đậm ngay khi tạo nút. Không viết cứng con
số padding nào — lấy phần khung bằng cách trừ bề rộng chữ khỏi `sizeHint()`,
rồi cộng lại bề rộng chính chữ ấy ở độ đậm 600, nên QSS đổi padding thì phép
đo tự theo. Vá cả đường đổi nhãn khi chuyển ngôn ngữ, nếu không đổi sang
tiếng Anh xong bề rộng vẫn giữ theo nhãn tiếng Việt cũ.
`SegmentedControl` phải tách ra file riêng vì `ui/widgets.py` đang ở đúng 505
dòng mã = đúng trần bánh cóc của cổng LOC, thêm một dòng là cổng đỏ. File cũ
giảm còn 466 dòng và vẫn nối lại tên cũ nên hai chỗ đang import không phải
sửa gì.
Kiểm cả 3 ngôn ngữ: 18/18 nút đều đủ chỗ.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Hồi quy đã vá
-------------
F-12 Kéo–thả hoặc dán tệp vào ô chat ném NameError. R08 tách `_Input` sang
`chat_input_box.py` nhưng để `_paths_from_mime()` ở lại
`composer_widget.py`, nên hai hàm sự kiện Qt gọi một cái tên không tồn
tại. Bốn hàm dùng chung chuyển sang `composer_mime.py` — module thứ ba
là chỗ duy nhất không lặp lại được lỗi này. Đo lại: cả thả lẫn dán đều
gắn 1 tệp, khớp bản trước refactor.
F-01 Đổi provider thì bộ chọn model AI-Edit không làm gì. Hook cũ kiểm
`folder.ai_model_combo`, thuộc tính R08-T12 đã dời sang
`ai_panel.resolver`. Làm mới vô điều kiện, đúng như tab cũ: lần lấy đầu
tiên hỏng thì đổi provider chính là lúc phải thử lại.
F-07 Hàng chọn kỳ của Dashboard bị đẩy xuống dưới các thẻ số liệu. Hàng này
lọc CẢ BA thẻ con chứ không riêng biểu đồ, nên để nó nằm dưới là bắt
người dùng đọc con số trước khi thấy con số đó tính cho kỳ nào. Kèm
theo: `TokenUsageCardWidget` bị bỏ sót `setContentsMargins(0,0,0,0)`
mà hai thẻ con còn lại đã có, đẩy cả hàng thẻ lệch 9px.
`check_layout_geometry` nay khớp TỪNG BYTE với bản trước refactor.
F-11 Hai lớp khai trùng tên phương thức; Python giữ bản sau nên bản đầu là
mã chết. `co4e_tab.py::showEvent` bản đầu gọi `_narrow_guard.attach()`
và không bao giờ chạy.
Tách file (F-09)
----------------
Bốn file chạm trần 400 dòng, mỗi lần cắt ra một trách nhiệm thật:
graph_renderer.py -> graph_scene_builder.py + graph_export.py
co4e_workflow_service.py -> co4e_run_history.py
json_config_repository.py -> config_sections.py
agents_admin_tab.py -> shared/agent_kind_visuals.py
File cuối còn xoá 3 bản sao của hàm đã có trong `shared/formatters.py`,
giống hệt đến từng dòng — nay định dạng thời gian và avatar không lệch nhau
giữa các bảng Giám sát nữa.
Docstring
---------
41,6% -> 100% (3.478/3.478 định nghĩa production), kể cả module dormant và
phương thức dunder. Toàn bộ phần bổ sung viết bằng tiếng Việt; comment tiếng
Anh có sẵn giữ nguyên — dịch ngược là một đợt riêng.
Seam chưa nối dây (F-05)
------------------------
9 seam mang nhãn `SEAM · dựng <ngày>` kèm hai câu: được nối khi nào, và để
dormant thì hỏng gì. Ngày lấy từ lịch sử git, không phải hạn tự đặt. Gate O
đọc nhãn đó và nhắc khi quá 30 ngày.
859 test xanh · 4/4 cổng CASAN · 19/24 checker khớp từng byte bản cũ.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Thư mục gốc: 22 file .py -> 7
--------------------------------
13 file "thành phần" nằm rải rác ngay ngoài thư mục gốc, mỗi file chỉ có ĐÚNG
MỘT nơi import — chính cái hub của nó:
i18n.py + 10 file i18n_*.py -> i18n/__init__.py + i18n/*.py
theme.py + 3 file theme_*.py -> theme/__init__.py + theme/*.py
Đổi hub thành `__init__.py` nên 78 chỗ `from ..i18n import tr` và 24 chỗ
`from ..theme import current_palette` KHÔNG phải sửa một dòng nào. Git nhận ra
11/15 file là đổi tên thuần, 0 dòng thay đổi; 4 file còn lại chỉ sửa đúng dòng
import và mấy tham chiếu tên file trong docstring.
Đối chiếu với bản trước khi gom, cùng một phép băm:
số khoá i18n 1431 -> 1431 hash STRINGS a06cc34b... (trùng)
QSS dark hash 7bb230a4... (trùng)
QSS light hash 884f73ce... (trùng)
`check_loc.py` phải khai thêm "i18n", "theme" vào DEFAULT_TARGET_DIRS: chúng
từng được quét theo diện "module nằm ở thư mục gốc", gom vào gói rồi thì không
khai là lặng lẽ tuột khỏi tầm quét.
Bánh cóc `ui/widgets.py` siết 505 -> 466 sau khi tách SegmentedControl — nợ cũ
co lại thì con số phải co theo, không thì bánh cóc đứng yên mãi ở mức cũ.
Một file requirements
---------------------
Xoá `requirements-test.txt`. Nó chỉ có `pytest` + `pydantic`, nhưng 64/108 file
test dựng widget thật và 20 file trong đó import PySide6 thẳng ở đầu file không
có bảo vệ — nên CI cài mỗi file kia thì pytest chết ngay lúc thu thập test chứ
không phải "vài test bị bỏ qua". Hai file cho một danh sách gần trùng nhau chỉ
tạo thêm một chỗ để lệch phiên bản, và `pydantic` đã bị chép ở cả hai.
CI đổi sang cài `requirements.txt`. Người dùng cuối cài thừa pytest vài MB.
Kèm theo: `install.bat` bỏ cờ `--dev` (không còn gì để cài thêm). Khối `if`
rỗng còn sót lại làm cmd.exe báo "( was unexpected at this time" và script chết
ngay sau bước cài thư viện — đã gỡ hẳn.
859 test xanh · 4/4 cổng CASAN · check_design_parity 32/32 ·
check_layout_geometry trùng từng byte với bản trước refactor.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Kéo 7 commit mới từ remote — chủ yếu dọn dẹp và siết chất lượng, không đổi
API đang dùng:
- Xoá 1.400 dòng mã chết + 5 gói rỗng còn sót sau các lần merge trước.
- Thêm CASAN Gate O (LOC) áp cho toàn cây mã.
- Sửa 5 checker UI hỏng sau đợt tách widget R08, vá 4 hồi quy.
- install.bat/run.bat, gộp requirements-test.txt vào requirements.txt.
- Gom i18n_*.py / theme_*.py rời rạc thành gói i18n/ và theme/.
Merge sạch, không có conflict marker nào (git tự resolve toàn bộ). Đã kiểm
tra lại 4 điểm đã vá ở 2 lần merge trước (config.py circular import,
ai_edit_model_resolver.py dùng resolve() thay route_turn(),
_confirm_routing_switch nhận timeout, RoutingApplicationService.resolve())
— cả 4 vẫn nguyên vẹn sau merge này.
pytest tests/: 793 passed — giống hệt số liệu trước khi merge, không phát
sinh fail/error mới (8 fail còn lại vẫn là do môi trường sandbox: thiếu
keyring, tên thư mục cowork-local vs cowork_local).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Summary
What changed and why?
Change Type
Related Work
Cowork Task:
Core Repo: http://34.143.229.138/gitea-admin/fsg-ai-core-assets
Core AI Issue:
Core Task:
Related PR:
Scope
What is intentionally included?
What is intentionally NOT included?
Validation
Commands / evidence:
Security Impact
Permission / credential / network / customer data impact:
Compatibility
Reviewer Notes
Anything Cowork reviewers should pay attention to.
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>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>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>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>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>Đổ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 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>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>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>Mô tả hệ thống ĐANG CHẠY, không phải hệ thống mong muốn. Mọi khẳng định chỉ tới file:dòng cụ thể, và mỗi tham chiếu đã được kiểm bằng script: mở đúng file, đọc đúng dòng, đối chiếu nội dung có khớp điều đang nói không. Lần kiểm đầu bắt được 3 tham chiếu thiếu tiền tố core/ và 2 số dòng lệch — dòng 249 là "No-op for any other tool", câu về bộ phân loại luôn bật nằm ở 250. Bốn điểm đáng chú ý trong tài liệu: - Đây KHÔNG phải rào chắn an ninh. Chính agent_security.py nói vậy ở đầu file, và hệ quả là mọi tầng AI đều mở khi hỏng. Ai đọc để đánh giá rủi ro phải hiểu đúng chỗ này. - Phân biệt quy tắc xác định và quy tắc do AI phán. Tắt hết công tắc trong màn Cài đặt thì VẪN còn bộ phân loại mẫu và sandbox — đây là điểm dễ hiểu nhầm nhất, vì mấy công tắc đó chỉ tắt phần AI. - Trạng thái thứ ba: hỏi người dùng. Hệ thống đã có (chat_panel.py:1312) mà chưa gọi tên; tool_policy.py gộp thành ALLOW/DENY/ASK. - Mục 8 liệt kê 4 chỗ đã biết là yếu, để người sau khỏi tưởng đã kín: mở khi hỏng, bí mật vẫn đi trong bộ nhớ (hệ quả của đường A), bộ luật OneDrive không ký số, và ASK chưa nối được vào Co4E. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>EPIC R03 (Team Duy) — Model Providers & Routing. All six tasks done. R03-T02 — Provider catalogue domain/models/provider_descriptor.py ProviderDescriptor (frozen), WireProtocol, AuthKind infrastructure/providers/provider_registry.py thread-safe registry: id/alias lookup, dynamic lookup by model id, adapter selection by protocol providers/factory.py drops its own _REGISTRY table and delegates to the registry, still raising ProviderError for callers R03-T03 — RoutingApplicationService (pure Python, 4 modes) application/model_routing/routing_models.py RoutingMode (off/auto/manual/fallback), RoutingRequest (immutable snapshot), RouteEvaluation, RoutingOutcome application/model_routing/routing_application_service.py the single decision flow, reached through two narrow ports plus a caller-supplied confirm callback, so no Qt import is needed application/model_routing/core_routing_adapter.py binds the ports to core/routing and AppContext Fallback is a new resilience mode: keep the selected model while it can serve the turn, re-route only when it cannot. Wired end to end through config.py, state.py, ui/routing_toggle.py and i18n.py (EN/JA/VI). R03-T04 / T05 — Remove the duplicated routing flow ui/chat_panel.py (#L638), ui/co4e_tab.py, ui/folder_tab.py each drop ~35 lines of copied logic and call the shared service; the widgets now only build a RoutingRequest, host the Manual-mode modal and render the outcome. R03-T06 — Token usage as an event infrastructure/telemetry/usage_sink.py UsageEvent + UsageEventSink protocol, with tracker, in-memory and composite sinks providers/openai_compat.py, providers/anthropic.py publish a UsageEvent instead of writing to the usage tracker themselves core/usage_tracker.py adds current_context() so a sink can borrow and restore a thread's attribution R03-T01 — Contract tests tests/contracts/test_providers.py parametrises over every provider in the registry: chat() signature, canonical assistant message, normalised tool calls, response closed, tool schema translation, ProviderError, list_models/test_connection, one UsageEvent per turn. Test infrastructure fix (required to verify any of the above): tests/conftest.py used to put the repository's PARENT directory on sys.path, so `import cowork_local.*` resolved against whichever sibling folder happened to carry that name — on a dev machine, an unrelated older checkout. The suite reported green while exercising different code. The conftest now binds this checkout to the cowork_local name in sys.modules. Verification pytest tests/ 236 passed in ~1.8s (102 before this change) scripts/check_imports.py PASS, 0 forbidden imports in domain/ and application/ new production files largest is 288 lines, all under the 400 LOC ceiling new tests 134 (50 contract, 70 unit, 14 integration), all offline scripts/run_quality_gate.py does not exist yet (R10-T02), so DoD item 7 was covered by check_imports.py plus the full suite. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>Dòng 31 ghi `secrets/`. Mẫu không neo, nên git bỏ qua MỌI thư mục tên secrets ở mọi độ sâu — kể cả infrastructure/secrets/ vốn là mã nguồn. Ba file ở đó chưa bao giờ lên repo. Máy tôi vẫn 150 test xanh vì pytest đọc đĩa chứ không đọc git; ai clone sạch thì đỏ 4 file ngay lúc thu thập: ModuleNotFoundError: No module named 'cowork_local.infrastructure.secrets' Hiệp phát hiện, không phải tôi. Đã dựng lại bằng clone sạch vào thư mục đặt đúng tên cowork_local để tái hiện. Neo mẫu thành /secrets/ và thêm tests/test_no_ignored_source.py — hỏi thẳng git chứ không hỏi đĩa, nên lần sau lỗi cùng hình dạng sẽ đỏ ngay trên máy người viết. Đã kiểm ngược: trả lại `secrets/` thì cả ba bài đỏ. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>Sau khi vá secrets/ thì rà cả file xem còn mẫu không neo nào sắp cắn hai người kia. Còn hai quả đang sống: logs/ -> nuốt infrastructure/logs/ (Hiệp làm CanonicalAuditLogger, đây là tên rất dễ đặt) build/ -> nuốt application/*/build/ dist/, out/ cùng kiểu Chưa ai vấp, vá trước. Trong repo không có build//dist//out//logs/ lồng nhau nào nên neo về gốc không mất gì — đã kiểm hai chiều: đường dẫn mã nguồn qua được, còn build/x.o, dist/app.exe, logs/run.log ở gốc vẫn bị chặn như cũ. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>R04-T01 — `domain/agents/conversation_execution_request.py`: a frozen snapshot of everything one chat turn needs. Turn inputs previously lived in a closure plus a 15-key ctx dict inside `ui/chat_panel.py::_start_turn`, and the worker thread kept reading the widget back while it ran, so every later click was visible to work already in flight. The request also owns the prompt composition rules (instruction prefix separator, session notes, model-switch review note) that were inline in that closure. R04-T02 — `domain/agents/agent_event.py`: 13 frozen event types replacing the untyped `{"type": ...}` dicts, whose only specification was the 130-line if/elif chain in `_on_event`. Each event serialises back to the exact legacy dict, so the presentation layer is untouched; `agent_event_codec.py` parses the other way and is a temporary shim, isolated so R08 can delete it in one move. `assistant_done` is deliberately NOT the end of a turn (it fires once per provider call), so it maps to AssistantMessageCompletedEvent while the new TurnCompletedEvent reports the turn itself. R04-T03 (part) — `domain/agents/agent_result.py`: one named outcome for a finished turn, replacing the message list / 3-tuple / reconstructed-from-side- effects trio the three callers each read differently. Verification: 66 tests. Beyond the unit tests, `tests/integration/test_agent_event_bridge.py` runs the REAL `run_cowork` loop offline and asserts every dict it emits is recognised and round-trips byte-for-byte — a guard against an event type nobody modelled or a key whose meaning silently drifted. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>Bắt được nhờ merge Delta: bộ test của họ chạy lâu hơn nên lộ ra một bài của tôi chập chờn. Truy ra không phải lỗi test mà là lỗi thật trong code chạy máy người dùng: PermissionError: [WinError 5] Access is denied .dem.json.l7x2a8pd.tmp -> dem.json MoveFileEx trả ERROR_ACCESS_DENIED khi tiến trình khác đang giữ handle lên nguồn hoặc đích — trên Windows gần như luôn là Defender hoặc Search Indexer quét file vừa tạo, giữ vài chục mili-giây rồi nhả. Đo được: hỏng 1 trong 7 lượt chạy 20 lần ghi, tức khoảng 1 trên 140 lần lưu. Nghĩa là người dùng thỉnh thoảng bấm Lưu là văng lỗi, và không tài nào tái hiện được để báo. Thêm vòng thử lại 6 lượt, nghỉ tăng dần 20ms → 640ms. Hết lượt vẫn ném lỗi, không nuốt lỗi quyền thật, và luôn dọn file tạm. Hai bài test mới, đã kiểm ngược: bỏ vòng thử lại thì bài thứ nhất đỏ. Chạy lại 30 lượt sau khi vá: 0 hỏng (trước khi vá: 4). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>Bốn mục trong Cài đặt tách thành widget riêng dưới presentation/settings/: general_settings_widget.py ngôn ngữ, giao diện, khay, gợi ý provider_settings_widget.py provider, base URL, key, model + 2 nút nền parameter_settings_widget.py đính kèm, cấu trúc, giới hạn sandbox routing_settings_widget.py Auto Model Routing Mỗi widget tự dựng control, tự nạp giá trị, tự có apply_to(data). Dialog chỉ còn lắp ráp và gọi apply_to lúc lưu — _save từ 34 dòng xuống còn phần khung. Làm lưới an toàn trước khi bóc: tests/ui/test_settings_dialog_dac_ta.py, 7 bài đặc tả hành vi hiện tại (mục nào có mặt, nạp đúng giá trị gì, lưu ghi vào đúng ô nào, đổi % sang phân lẻ, xoá cache sau lưu). Bóc xong cả 7 vẫn xanh, và trong lúc bóc chúng đã đỏ đúng hai lần ở chỗ đáng đỏ. Đây là repo chưa từng có test Qt nào — thêm tests/ui/conftest.py dựng QApplication offscreen. Offscreen là bắt buộc chứ không phải cho nhanh: máy dev là máy làm việc thật, test bật cửa sổ lên là nó nhảy ra che màn hình. Dọn kèm: * bỏ vòng "dựng vào layout rồi lại gỡ ra" của mục Chung, cùng widget cao 0px làm mốc cuộn — không cần nữa khi mục đó tự là một widget * bỏ _select_combo, _secret, _model_combo, _with_load và 4 hàm provider khác đã chuyển vào widget (127 dòng) * bỏ 5 import chết theo (Dict, QSizePolicy, PROVIDER_LABELS, SegmentedControl, LANGUAGES) Giữ cầu tương thích: self.routing_*, self.prov_*, self.attach_* … thành property trỏ vào widget con, vì 5 checker trong tools/ đọc thẳng tên cũ. Bỏ được khi tools/ chuyển sang đọc self._provider_page. Hai điều KHÔNG làm, ghi lại để khỏi tưởng là quên: 1. Plan ghi 4 widget và có tên `connector`. Thực tế UI connector đã dời khỏi Cài đặt từ trước (ghi chú ở settings_dialog.py:180 bản cũ), nên số mục thật là 5, không phải 4, và không có mục nào tên connector. Bốn mục bóc ra là 4 mục có thật; mục Bảo mật sandbox để nguyên trong dialog lần này. 2. Còn ~108 dòng chết của MS365 (_refresh_ms365_status, _ms365_sign_in, _show_ms365_device_code, _ms365_sign_out): đọc self.ms365_status, self.ms365_signin_btn, self.ms365_signout_btn — ba thuộc tính KHÔNG BAO GIỜ được gán, và không hàm nào có người gọi. Gọi vào là AttributeError. Chưa xoá vì đó là quyết định của anh Nam, không phải việc kèm theo của T07. 437 test xanh. check_dialogs, check_no_hscroll, check_design_parity đều qua. Kèm docs/refactor/tin-gui-team-hoa.md — tin báo Hoa về platform/ -> adapters/ và bản vá Windows của AtomicJsonFile. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>app.py 1356 -> 1293 dòng. presentation/shell/ có 3 file: bootstrap.py Composition Root (đã vào ở commit trước) tray_manager.py khay hệ thống + thông báo bong bóng lifecycle_coordinator.py canh cửa sổ theo màn hình + tắt cho sạch Vì sao tách khay: khay là thứ CÓ THỂ KHÔNG TỒN TẠI (một số môi trường Linux, phiên RDP). Trước đây mỗi chỗ dùng phải tự nhớ kiểm `if self.tray is not None` — có 6 chỗ như thế, và 3 chỗ còn phải tự bọc try/except quanh showMessage. Gói lại thì chỗ gọi cứ gọi, không có khay thì không có gì xảy ra. Vì sao tách vòng đời: hai việc trong đó không phải việc của giao diện. Canh cửa sổ theo màn hình là số học thuần (anh Nam có hai màn khác độ phân giải và khác tỉ lệ phóng — kéo qua lại là vùng làm việc đổi). Còn shutdown là thứ tự dừng có ý nghĩa: bộ lập lịch trước để nó không kịp khởi động việc mới trong lúc ta đang dừng việc cũ, rồi mới tới worker, rồi ngắt tiến trình MCP. closeEvent/moveEvent/resizeEvent vẫn ở lớp cửa sổ vì Qt gọi thẳng vào đó, nhưng phần quyết định đã chuyển đi. closeEvent từ 30 dòng còn 11. Giữ self.tray thành property trỏ vào self._tray.icon — vài chỗ còn đọc tên cũ. Đã lấy mốc trước khi bóc rồi so lại sau: 24/24 checker trong tools/ qua cả hai lần. Đây là bộ đặc tả thật cho MainWindow (check_nav, check_rail_align, check_layout_geometry, check_controls_alive... dựng cửa sổ thật offscreen trên BẢN SAO của ~/.cowork_local, scheduler bị vô hiệu hoá). 632 test xanh. CHƯA làm hết R08-T10: plan ghi tách thành main_window.py + tray_manager.py + lifecycle_coordinator.py. Hai file sau đã xong, main_window.py thì chưa — MainWindow vẫn nằm trong app.py và vẫn 1095 dòng. Đo lại thì khối lượng không nằm ở ba cụm plan nêu mà ở hai cụm khác: nav rail 18 method, ~340 dòng topbar 8 method, ~157 dòng __init__ 279 dòng Hai cụm đó dính chặt vào state của cửa sổ, chuyển đi cần đổi giao diện giữa chúng chứ không phải dời chỗ, nên tôi dừng ở đây thay vì làm nửa vời. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>Sót lại từ lần dời UI Connector sang Monitoring → Tools → Connector. Năm hàm: _refresh_ms365_status 13 _show_ms365_device_code 52 _ms365_sign_in 34 _ms365_sign_out 4 _close_ms365_code_dialog 5 Chứng minh chết trước khi xoá, không xoá theo cảm tính: * Dựng đồ thị lời gọi bằng ast: **mọi** lời gọi tới năm hàm này đều xuất phát từ bên trong chính năm hàm đó. Không một đường vào nào từ ngoài cụm — cả trong file lẫn toàn repo. * Ba thuộc tính chúng đọc — ms365_status, ms365_signin_btn, ms365_signout_btn — **chưa từng được gán ở đâu**. Gọi vào là AttributeError, không phải chạy sai mà là sập. * _ms365_workers chỉ được append bên trong _ms365_sign_in, nên chết theo. Dọn kèm 6 import chỉ còn dòng import: AgentWorker, icon, EXT_CATEGORIES, ExtConnectorEditDialog, AppContext, QTreeWidget. Viết lại docstring đầu file — bản cũ vẫn mô tả file này chứa nhóm Connector (CAD/CAE/MS365/Other), thứ đã không còn ở đây từ lâu. settings_dialog.py: 407 -> 303 dòng. Cộng cả R08-T07 thì từ 727 xuống 303. 632 test xanh. check_dialogs, check_no_hscroll, check_design_parity, check_orphans, check_probes_bite đều qua. Ghi lại một phát hiện phụ, CHƯA xử lý: i18n.py có 28 khoá settings.ms365_* mồ côi — 20 khoá đã không ai dùng từ trước lần dời connector, 8 khoá vừa mồ côi theo commit này. Chỉ 2 khoá còn sống (ms365_local_connected, ms365_local_none, dùng ở ui/connectors_panel.py). Xoá khoá dịch là đụng vào dữ liệu ba ngôn ngữ ở file khác nên để anh Nam quyết riêng. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>Đây là deliverable còn thiếu duy nhất trong 17 task của Gamma. app.py 128 chỉ còn điểm vào chương trình presentation/shell/ main_window.py 362 __init__ + vòng đời cửa sổ nav_rail.py 385 dựng rail + cây điều hướng + thu gọn top_bar.py 234 thanh trên + tài khoản + đáy rail session_events.py 104 lịch sử, thông báo task xong page_registry.py 82 4 màn chính, dựng lười, _goto rail_project.py 132 bộ chọn project + RECENTS lifecycle_coordinator.py 110 canh màn hình + tắt sạch tray_manager.py 76 khay hệ thống toast.py 40 thông báo góc trên trái bootstrap.py 42 Composition Root branding.py 26 ASSETS + app_icon rail_metrics.py 37 kích thước rail + cách vẽ hàng Mọi file dưới 400 dòng. Đây là ngưỡng CASAN Check 2. NÓI THẲNG VỀ CÁCH TÁCH: sáu file trong đó là MIXIN, không phải widget rời. Cả loạt phương thức đọc/ghi state của cửa sổ (self._page_widgets, self.workspace, self.splitter...). Biến thành đối tượng cộng tác thì phải viết lại từng chỗ self.X thành self.window.X — gần 800 dòng sửa chỉ để đổi cách gọi, rủi ro cao mà không đổi hành vi. Mixin cho đúng thứ đang cần: mỗi mảng một file, ai sửa rail thì mở file rail. Chuyển thành widget thật khi có cửa sổ thứ hai cần dùng lại — hiện chưa có. Giữ đường vào cũ: MainWindow, app_icon, _NAV_*, _Toast vẫn import được từ cowork_local.app, nên 24 checker trong tools/ không phải sửa. BA LỖI TỰ GÂY TRONG LÚC TÁCH, ĐỀU DO CHECKER BẮT ------------------------------------------------- 1. 12 import lazy nằm trong thân hàm bị thụt lề nên regex đổi mức tương đối của tôi bỏ sót -> ModuleNotFoundError khi bấm vào rail. 2. Bộ dò import thiếu của tôi tính cả import cục bộ trong hàm KHÁC, nên tưởng QHBoxLayout đã có -> 17 checker đỏ. Bỏ cách dò, cấp thẳng khối import đầy đủ rồi cắt phần không dùng. 3. Hằng số ASSETS và _NAV_* nằm ở khối tôi không mang theo -> NameError. Cả ba đều là lỗi im lặng với bộ test đơn vị (714 vẫn xanh suốt) và chỉ lộ khi dựng cửa sổ thật. Đó chính là lý do bộ checker trong tools/ tồn tại. Cập nhật 2 đích đột biến của check_probes_bite: mã nó cần sửa đã dời khỏi app.py sang rail_project.py và nav_rail.py. 714 test xanh. 24/24 checker qua. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>File to nhất còn lại của Gamma. Lâm bàn giao ở 1.885 dòng với 100 method trong một lớp; chia theo bảy mối quan tâm: co4e_runs.py 364 chạy flow, 3 chế độ, bảng lịch sử lượt chạy co4e_chat.py 343 khung chat + đếm token + định tuyến riêng co4e_layout.py 308 ba khung, bảng cấu hình, bố cục màn hẹp co4e_sidebar.py 251 thư viện workflow/agent/skill, 4 mục gập co4e_flow_tabs.py 180 dải tab các flow đang mở co4e_workflow_crud.py 154 tạo/sửa/xoá/nhân bản workflow co4e_agents.py 51 agent và skill dùng trong flow ui/co4e_tab.py 389 __init__, set_project, thư mục output Mọi file dưới 400 dòng. MỘT LỖI SUÝT LÀM HỎNG FILE: bản đầu tôi cắt method theo m.lineno, mà lineno trỏ vào dòng `def`, không tính dòng `@...` phía trên. Decorator bị bỏ lại thành mồ côi ngay trên một hằng số lớp -> file hỏng cú pháp. Bắt được vì script tự parse lại sau mỗi lần cắt; nếu chỉ cắt rồi ghi thì đã đẩy lên một file không import nổi. Ba vòng sửa mức import tương đối: co4e_tab.py nằm ở ui/ (1 cấp), file mới ở presentation/co4e/ (2 cấp). Còn co4e_canvas / co4e_config_panel / co4e_agent_dialog thì VẪN ở ui/, nên `.co4e_canvas` phải thành `...ui.co4e_canvas` chứ không phải `.co4e_canvas` cùng thư mục. 714 test xanh — trong đó có ~4.000 dòng test đặc tả Lâm viết cho đúng vùng này, nên việc tách được soi khá kỹ. check_co4e, check_controls_alive, check_layout_geometry, check_probes_bite đều qua. Cập nhật đích đột biến thứ ba của check_probes_bite: dải tab flow nay ở presentation/co4e/co4e_layout.py. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>Ba file dữ liệu cuối cùng của Gamma còn trên ngưỡng CASAN Check 2. i18n.py 3075 -> 94 Dict STRINGS 3.000 dòng cắt thành 10 cụm theo đúng mốc phân đoạn có sẵn trong file (mỗi mốc là một màn/hộp thoại), cụm nào quá dài thì cắt tiếp ở ranh giới khoá. i18n.py giờ chỉ gộp lại và giữ 4 hàm set_language/ get_language/tr/on_language_changed. Kiểm bằng cách so với bản gốc lấy từ git: 1437 mục / 1431 khoá duy nhất (bản gốc vốn có 6 khoá lặp), sau khi chia vẫn 1431, KHÔNG thiếu khoá nào, KHÔNG thừa khoá nào, KHÔNG giá trị nào lệch. Thứ tự gộp giữ nguyên nên quy tắc "khoá trùng thì bản sau thắng" không đổi. theme.py 907 -> 130 theme_palettes.py 328 hai bảng màu Tối/Sáng + lớp Palette theme_qss.py 198 nửa vỏ (reset + shell) theme_qss_controls.py 307 nửa điều khiển (nút, ô nhập, tab, badge) Khuôn QSS 470 dòng cắt đôi đúng mốc `/* ---- surfaces */` của chính nó. Đã đối chiếu: stylesheet('dark') ra đúng 24762 ký tự y như trước — khớp từng byte, không phải "trông có vẻ giống". core/usage_tracker.py 536 -> 307 usage_cost.py 101 bảng giá, quy đổi token sang tiền, định dạng usage_periods.py 144 gộp theo ngày/tuần/tháng/quý, chuỗi vẽ biểu đồ usage_ai_report.py 56 dựng câu nhắc cho AI phân tích HAI LẦN TỰ CẮT HỎNG, ĐỀU CÙNG MỘT GỐC -------------------------------------- 1. Cắt theo m.lineno mà quên dòng @decorator phía trên -> @dataclass của Palette bị bỏ lại mồ côi, "Palette() takes no arguments". 2. Đọc số dòng từ AST GỐC trong khi danh sách dòng đã bị cắt -> lần bóc thứ hai dùng toạ độ cũ và cắt vào giữa một chữ ký hàm. Cả hai lộ ngay vì mỗi script tự parse lại sau khi ghi. Bài học đã áp vào cả ba lần chia: parse lại sau mỗi lần cắt, và luôn tính cả decorator. KẾT QUẢ CASAN CHECK 2 --------------------- Nam 0 file vượt 400 (trước: 4, tổng 5.898 dòng) Hiệp 0 (trước: 1) Lâm 0 (trước: 1) file mới 0 (61 file dưới presentation/ application/ domain/ infrastructure/ — chưa cái nào vượt) Gamma sạch. 23 file còn vượt đều thuộc team khác (chat_panel.py 1802, folder_tab.py 1589, structure_graph_view.py 1034...) — cần báo lên sớm chứ đừng để tới hạn 30/08 mới lộ. 714 test xanh. 24/24 checker qua. CASAN Check 1 sạch. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>Team Hoa, EPIC R08 (UI/Application Separation) - Team Hoa scope only (R08-T11 -> T14; R08-T01->T10 belong to Team Duy/Team Nam). - R08-T11: ui/schedule_task_tab.py (795 lines) -> presentation/scheduling/ {kanban_board_widget,calendar_view_widget,ai_task_creator_dialog, ai_task_import_dialog,run_history_dialog}.py + schedule_task_tab.py shell. Kanban CRUD/drag-drop now goes through application/scheduling/task_application_service.py (R07-T04) instead of ~30 lines of inline if/elif per drag target. - R08-T12: ui/folder_tab.py (1587 lines, the largest of the four) -> presentation/folder/{workspace_file_tree,document_preview_manager, code_editor,office_document_renderer,ai_file_editor_dialog, ai_edit_model_resolver,ai_edit_pipeline}.py + folder_tab.py shell. Closes the R06-T05 loop: FileWorkspaceService existed since R06 with zero production call sites (confirmed by grep); every plain-text write (save/create/write_content) now goes through it, gaining path containment and a Python-syntax warning the original code never had. Pure helpers (_read_text, _is_probably_text, _pptx_available, _split_code_block, _parse_ai_output) moved to application/workspaces/{file_preview_helpers,ai_edit_output}.py. - R08-T13: ui/dashboard_tab.py (437 lines) -> presentation/dashboard/ {token_usage_card_widget,usage_chart_widget,habits_widget}.py + dashboard_tab.py shell, backed by a new application/monitoring/dashboard_query_service.py (pricing/period/ summary queries the three widgets used to each recompute separately). Directory-ownership note left in the checklist for Team Nam. - R08-T14: ui/structure_graph_view.py (1035 lines) -> presentation/graph/{graph_scene_items,graph_renderer, graph_messages_view,graph_qa_widget}.py + structure_graph_view.py shell. Extraction helpers (_pdf_to_markdown, _extract_file_contents) moved to application/workspaces/graph_index_service.py (pure Python). Renderer and Q&A panel talk only through signals (node_selected/graph_rendered/raw_json_ready/project_changed) - neither imports the other. - presentation/shared/web_engine_support.py: HAS_WEB_ENGINE, previously duplicated (folder_tab imported it FROM structure_graph_view.py) - now one shared flag instead of one screen importing another screen's module. All four old ui/*.py files deleted; app.py and ui/workspace_tab.py updated to the new import paths (each god-file only had 1-2 real construction sites, so import sites were updated directly rather than kept as a strangler-fig shim - unlike core/tools.py at R05, which had dozens). pytest: 377 pass (+94 vs the R07 baseline of 328; same 4 pre-existing failures as the R05/R06 baseline, unrelated to this work). scripts/check_imports.py: PASS. python -c "import cowork_local.app": OK. Every new file < 400 lines (largest: graph_renderer.py, 391). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>token_usage_card_widget.py 93 5 thẻ số liệu + thẻ Ngân sách usage_chart_widget.py 119 biểu đồ tuần/tháng/năm + đường so sánh habits_widget.py 171 thói quen dùng token + nhận xét của AI Ba widget THẬT, không phải mixin — khác với shell và Co4E, ba mảng này tách bạch trên màn hình và không đọc state của nhau. Giao tiếp bằng signal: budget_applied, filter_changed, status_message. Điểm cần biết: các nút lật khoảng và hai ô chọn thuộc về UsageChartWidget nhưng được Dashboard nhấc lên hàng điều khiển ở trên. Chúng là control của biểu đồ, chỉ hiển thị ở chỗ khác. Giữ 18 cầu tương thích cho tên cũ vì check_dashboard, check_design_parity và check_controls_alive đọc thẳng self.card_total, self._chart_period_lbl... 756 test xanh. check_dashboard, check_design_parity, check_controls_alive qua. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>presentation/graph/ structure_graph_view.py 325 lớp chính + dựng giao diện graph_qa_widget.py 322 hỏi-đáp trên đồ thị (_ask 119 dòng) graph_render.py 226 quét, vẽ Qt + D3, xuất ảnh graph_scene.py 138 node, cạnh, khung nhìn — thuần đồ hoạ graph_project.py 109 chọn project, đổi tab xem graph_web.py 38 cờ có dùng được QtWebEngine không ui/structure_graph_view.py 11 vỏ chuyển tiếp, giữ đường import cũ BA LẦN CẮT HỎNG, ĐỀU LÀ TÊN CẤP MODULE BỊ BỎ LẠI ------------------------------------------------ _HAS_WEB, QWebEngineView, QWebChannel, _Bridge, _Edge, _Node — tất cả định nghĩa ở file gốc, dùng ở file mới, nên NameError ngay lúc chạy. Bộ test đơn vị KHÔNG bắt được cái nào: 756 bài vẫn xanh suốt ba lần. Chỉ check_graphrag_rescan bắt, vì nó gọi prewarm() thật rồi chờ đồ thị dựng xong. Sau lần thứ ba tôi bỏ cách đuổi từng lỗi và viết bộ dò tên chưa định nghĩa có tính đến phạm vi hàm (tham số, biến cục bộ, except-as, comprehension). Nó tìm ra nốt _fmt_plan và _qcolor còn thiếu ở hai file Co4E đã tách hôm trước — hai quả mìn chưa nổ. _HAS_WEB tách hẳn ra graph_web.py: cả structure_graph_view.py lẫn graph_render.py đều phải hỏi, để ở một trong hai là vòng import. 756 test xanh. 24/24 checker qua. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>presentation/scheduling/ calendar_view_widget.py 231 lịch tháng (chuyển từ ui/calendar_view.py) ai_task_creator_dialog.py 208 tạo task bằng AI task_actions.py 189 thêm/sửa/chạy/xoá/xem log một task kanban_board_widget.py 98 cột Kanban + vùng thả file run_history_dialog.py 82 lịch sử các lượt chạy ai_task_import_dialog.py 81 nhập task từ file ui/schedule_task_tab.py 297 dựng bảng + đổi chế độ xem ui/calendar_view.py 10 vỏ chuyển tiếp Plan ghi 4 file; thực tế cần 6. Hai file thêm là run_history_dialog.py và task_actions.py — không tách thì schedule_task_tab.py còn 517 dòng, vẫn vượt ngưỡng 400. ai_task_import_dialog.py làm mixin chứ không phải hộp thoại rời: plan gọi nó là dialog, nhưng thực tế nó là TAB THỨ HAI của cùng hộp thoại tạo task, dùng chung phần xem trước và nút Xác nhận. Tách hẳn thì phải nhân đôi cả hai. LẠI LỖI DECORATOR: script này tôi quên dùng bản có tính dòng @, nên một @staticmethod bị bỏ lại mồ côi -> IndentationError. Đây là lần thứ tư cùng một lỗi. Đã thêm bước dọn decorator mồ côi vào script. 756 test xanh. 16 checker chạy đều qua. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>presentation/folder/ ai_edit_runner.py 325 một lượt AI sửa file, từ gửi tới xem trước document_preview_manager.py 317 PDF/Word/Excel/PowerPoint/ảnh/HTML/mã ai_file_editor_dialog.py 317 dựng panel AI + chọn model code_editor.py 183 ô soạn mã, đánh số dòng, tô cú pháp ai_output_writer.py 140 phần DUY NHẤT chạm vào file người dùng image_model_picker.py 115 dò model sinh ảnh trên mọi provider file_helpers.py 112 nhận dạng loại file + ngưỡng workspace_file_tree.py 38 cây thư mục ui/folder_tab.py 305 lắp ráp + retranslate Plan ghi 3 file; khối lượng thật cần 8. Hai file tôi thêm ngoài dự kiến vì đọc kỹ thì chúng là ranh giới thật: * ai_output_writer.py — tách ra vì đây là phần duy nhất THẬT SỰ ghi đè file của người dùng. Mọi thứ trước nó chỉ dựng bản xem trước. Ranh giới đó đáng nhìn thấy trong cấu trúc thư mục. * image_model_picker.py — chỗ duy nhất trong màn Thư mục biết tới nhiều provider cùng lúc (nó gợi ý được model sinh ảnh của provider KHÁC cái đang chọn). Gom mọi hằng nhận dạng loại file (_IMAGE_SUFFIXES, _HAS_PDF, _MAX_EDIT_BYTES…) về file_helpers.py: cả tám file trong gói đều hỏi tới, để rải ra thì thêm một đuôi file phải sửa vài chỗ. LẠI IMPORT LAZY THỤT LỀ: regex đổi mức tương đối của tôi chỉ khớp đầu dòng nên bỏ sót import nằm trong thân hàm — 3 checker đỏ. Lần này tôi sửa một lượt cho CẢ cây presentation/ thay vì riêng thư mục vừa tách; nó tìm ra thêm 3 file ở scheduling cũng đang sai mà chưa nổ. 756 test xanh. 24/24 checker qua. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>presentation/chat/ chat_history_widget.py 348 T01 mạch hội thoại (từ ui/chat_view.py) chat_bubble_style.py 202 T01 cách vẽ bong bóng, diff, đường thời gian composer_widget.py 364 T02 thanh công cụ quanh ô nhập chat_input_box.py 328 T02 ô nhập: Ctrl+Enter, dán ảnh, popup /skill attachment_picker.py 215 T03 đọc tệp đính kèm + chặn theo chính sách chat_output_panel.py 186 T05 theo dõi thư mục output, hiện tệp mới chat_turn_runner.py 281 T06 chạy một lượt chat_event_stream.py 228 T06 nhận sự kiện phát về từ luồng nền chat_session_store.py 413 T06 lưu/nạp phiên, đếm token, nối lại lượt chat_agents.py 246 T06 chọn agent, skill, định tuyến model chat_panel_layout.py 148 T06 bố cục hai cột chat_helpers.py 53 T06 hàm và bảng tra dùng chung ui/chat_panel.py 345 __init__ + trạng thái ui/chat_view.py 10 vỏ chuyển tiếp ui/composer.py 11 vỏ chuyển tiếp R08-T04 KHÔNG LÀM ĐƯỢC: plan đòi audio_recorder_widget.py, nhưng trong repo KHÔNG CÓ chức năng ghi âm nào — grep 'audio|record|voice|micro' toàn ui/ chỉ ra chữ 'record' trong nghĩa 'ghi lại transcript'. Không có gì để tách, và tôi không dựng một widget mới nhân danh refactor. Giống hệt trường hợp connector_settings_widget.py ở T07. _start_turn (144 dòng) và _on_event (127) để nguyên có chủ ý: cái đầu dựng trọn ngữ cảnh một lượt rồi giao cho luồng nền, cái sau phân nhánh theo loại sự kiện. Cắt nhỏ thì phải chuyền hàng chục biến trạng thái qua lại, đọc khó hơn. Hai lỗi tự gây, cả hai đều do script: * regex bỏ import cũ chỉ cắt DÒNG ĐẦU của một import nhiều dòng, để lại phần đuôi mồ côi -> IndentationError. * _build_layout dùng biến 'root' vốn cục bộ trong __init__. Bộ test bắt được cái này (2 bài integration đỏ), không phải checker — vì nó là lỗi dựng widget, không phải lỗi hình học. 756 test xanh. 24/24 checker qua. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>Soát lại từng dòng plan thì thấy tôi báo R08 xong hơi sớm. Ba chỗ thiếu thật: T06 ChatPanel vẫn ở ui/, plan đòi presentation/chat/chat_panel.py T08 agents_admin_tab.py (498) và tools_admin_tab.py (245) vẫn ở ui/ presentation/chat/chat_panel.py 346 presentation/monitoring/tabs/agents_admin_tab.py 383 presentation/monitoring/tabs/agent_edit_dialog.py 143 presentation/monitoring/tabs/tools_admin_tab.py 245 ui/chat_panel.py / agents_admin_tab.py / tools_admin_tab.py ~10 mỗi cái agents_admin_tab.py 498 dòng nên tách thêm agent_edit_dialog.py: bảng danh sách và hộp thoại sửa là hai việc, và hộp thoại còn tự đi hỏi provider xem có model nào — thứ bảng không cần biết. BA CHỖ CÒN LẠI KHÔNG PHẢI THIẾU, đã kiểm từng cái: * audio_recorder_widget.py (T04) — repo KHÔNG có chức năng ghi âm nào. * connector_settings_widget.py (T07) — UI Connector đã dời khỏi Cài đặt. * sandbox_status_tab.py / mcp_history_tab.py (T08) — Hiệp đặt tên sandbox_tab và mcp_tab, nội dung đủ. R08: 14/14 task, 0 file thiếu thật sự. 756 test xanh. 24/24 checker qua. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>Trình chạy ---------- Cả hai lệnh trong README đều không chạy được từ một thư mục checkout tên khác `cowork_local`: python -m cowork_local -> No module named cowork_local python __main__.py -> ModuleNotFoundError: No module named 'cowork_local' Không sửa được bằng mẹo sys.path, vì `state.py` khởi động máy chủ MCP MS365 bằng tiến trình con `python -m cowork_local.mcp_servers.ms365_server` — tiến trình con cũng phải import được. Hai script tạo một junction ở `%LOCALAPPDATA%\CoworkLocal\launcher` thay vì bắt người dùng đổi tên thư mục làm việc. Môi trường ảo đặt ở `%LOCALAPPDATA%\CoworkLocal\venv`, cố ý KHÔNG đặt trong repo: các cổng chất lượng quét toàn bộ cây thư mục chứ không đọc `.gitignore`, nên một `.venv` ở đây sẽ biến vài nghìn module thư viện thành "mã production không ai import" và làm Gate O đỏ. requirements.txt ---------------- Chạy thử `run.bat` trên một profile trắng thì app chết ngay lúc mở: presentation/folder/code_editor.py:41 ModuleNotFoundError: No module named 'pygments' Quét toàn bộ import bên thứ ba thì thiếu 8 thư viện, trong đó `pygments` và `pydantic` là bắt buộc — import không có try/except, nên triệu chứng không phải "tính năng đó không chạy" mà là app không mở được. Nghĩa là cài đúng theo requirements.txt xong app vẫn hỏng. Đã tách rõ nhóm bắt buộc / tuỳ chọn kèm lý do từng dòng. `opendataloader-pdf` để nguyên dạng chú thích vì code tự cài khi cần qua `core/deps.py::ensure_module`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>`theme_qss.py` đặt `font-weight: 600` cho nút đang chọn, nhưng `QPushButton` tính `sizeHint()` theo phông thường. Chữ đậm rộng hơn — nên đúng lúc một mục được chọn thì nó không còn đủ chỗ và Qt cắt bớt chữ. Đo được trước khi vá: Tiếng Việt 85px cần 87px thiếu 2px English 67px cần 69px thiếu 2px Tự động (theo hệ thống) 170px cần 177px thiếu 7px 日本語 50px cần 50px — Tiếng Việt lộ rõ nhất vì nó vừa là nhãn dài nhất trong dải ngôn ngữ, vừa có dấu, và với người dùng tiếng Việt thì nó LUÔN là mục đang được chọn, tức luôn là mục bị in đậm. Chữ Nhật không dính vì bề rộng glyph CJK không đổi theo độ đậm. Cách vá: chừa sẵn bề rộng cho chữ đậm ngay khi tạo nút. Không viết cứng con số padding nào — lấy phần khung bằng cách trừ bề rộng chữ khỏi `sizeHint()`, rồi cộng lại bề rộng chính chữ ấy ở độ đậm 600, nên QSS đổi padding thì phép đo tự theo. Vá cả đường đổi nhãn khi chuyển ngôn ngữ, nếu không đổi sang tiếng Anh xong bề rộng vẫn giữ theo nhãn tiếng Việt cũ. `SegmentedControl` phải tách ra file riêng vì `ui/widgets.py` đang ở đúng 505 dòng mã = đúng trần bánh cóc của cổng LOC, thêm một dòng là cổng đỏ. File cũ giảm còn 466 dòng và vẫn nối lại tên cũ nên hai chỗ đang import không phải sửa gì. Kiểm cả 3 ngôn ngữ: 18/18 nút đều đủ chỗ. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>Hồi quy đã vá ------------- F-12 Kéo–thả hoặc dán tệp vào ô chat ném NameError. R08 tách `_Input` sang `chat_input_box.py` nhưng để `_paths_from_mime()` ở lại `composer_widget.py`, nên hai hàm sự kiện Qt gọi một cái tên không tồn tại. Bốn hàm dùng chung chuyển sang `composer_mime.py` — module thứ ba là chỗ duy nhất không lặp lại được lỗi này. Đo lại: cả thả lẫn dán đều gắn 1 tệp, khớp bản trước refactor. F-01 Đổi provider thì bộ chọn model AI-Edit không làm gì. Hook cũ kiểm `folder.ai_model_combo`, thuộc tính R08-T12 đã dời sang `ai_panel.resolver`. Làm mới vô điều kiện, đúng như tab cũ: lần lấy đầu tiên hỏng thì đổi provider chính là lúc phải thử lại. F-07 Hàng chọn kỳ của Dashboard bị đẩy xuống dưới các thẻ số liệu. Hàng này lọc CẢ BA thẻ con chứ không riêng biểu đồ, nên để nó nằm dưới là bắt người dùng đọc con số trước khi thấy con số đó tính cho kỳ nào. Kèm theo: `TokenUsageCardWidget` bị bỏ sót `setContentsMargins(0,0,0,0)` mà hai thẻ con còn lại đã có, đẩy cả hàng thẻ lệch 9px. `check_layout_geometry` nay khớp TỪNG BYTE với bản trước refactor. F-11 Hai lớp khai trùng tên phương thức; Python giữ bản sau nên bản đầu là mã chết. `co4e_tab.py::showEvent` bản đầu gọi `_narrow_guard.attach()` và không bao giờ chạy. Tách file (F-09) ---------------- Bốn file chạm trần 400 dòng, mỗi lần cắt ra một trách nhiệm thật: graph_renderer.py -> graph_scene_builder.py + graph_export.py co4e_workflow_service.py -> co4e_run_history.py json_config_repository.py -> config_sections.py agents_admin_tab.py -> shared/agent_kind_visuals.py File cuối còn xoá 3 bản sao của hàm đã có trong `shared/formatters.py`, giống hệt đến từng dòng — nay định dạng thời gian và avatar không lệch nhau giữa các bảng Giám sát nữa. Docstring --------- 41,6% -> 100% (3.478/3.478 định nghĩa production), kể cả module dormant và phương thức dunder. Toàn bộ phần bổ sung viết bằng tiếng Việt; comment tiếng Anh có sẵn giữ nguyên — dịch ngược là một đợt riêng. Seam chưa nối dây (F-05) ------------------------ 9 seam mang nhãn `SEAM · dựng <ngày>` kèm hai câu: được nối khi nào, và để dormant thì hỏng gì. Ngày lấy từ lịch sử git, không phải hạn tự đặt. Gate O đọc nhãn đó và nhắc khi quá 30 ngày. 859 test xanh · 4/4 cổng CASAN · 19/24 checker khớp từng byte bản cũ. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>Thư mục gốc: 22 file .py -> 7 -------------------------------- 13 file "thành phần" nằm rải rác ngay ngoài thư mục gốc, mỗi file chỉ có ĐÚNG MỘT nơi import — chính cái hub của nó: i18n.py + 10 file i18n_*.py -> i18n/__init__.py + i18n/*.py theme.py + 3 file theme_*.py -> theme/__init__.py + theme/*.py Đổi hub thành `__init__.py` nên 78 chỗ `from ..i18n import tr` và 24 chỗ `from ..theme import current_palette` KHÔNG phải sửa một dòng nào. Git nhận ra 11/15 file là đổi tên thuần, 0 dòng thay đổi; 4 file còn lại chỉ sửa đúng dòng import và mấy tham chiếu tên file trong docstring. Đối chiếu với bản trước khi gom, cùng một phép băm: số khoá i18n 1431 -> 1431 hash STRINGS a06cc34b... (trùng) QSS dark hash 7bb230a4... (trùng) QSS light hash 884f73ce... (trùng) `check_loc.py` phải khai thêm "i18n", "theme" vào DEFAULT_TARGET_DIRS: chúng từng được quét theo diện "module nằm ở thư mục gốc", gom vào gói rồi thì không khai là lặng lẽ tuột khỏi tầm quét. Bánh cóc `ui/widgets.py` siết 505 -> 466 sau khi tách SegmentedControl — nợ cũ co lại thì con số phải co theo, không thì bánh cóc đứng yên mãi ở mức cũ. Một file requirements --------------------- Xoá `requirements-test.txt`. Nó chỉ có `pytest` + `pydantic`, nhưng 64/108 file test dựng widget thật và 20 file trong đó import PySide6 thẳng ở đầu file không có bảo vệ — nên CI cài mỗi file kia thì pytest chết ngay lúc thu thập test chứ không phải "vài test bị bỏ qua". Hai file cho một danh sách gần trùng nhau chỉ tạo thêm một chỗ để lệch phiên bản, và `pydantic` đã bị chép ở cả hai. CI đổi sang cài `requirements.txt`. Người dùng cuối cài thừa pytest vài MB. Kèm theo: `install.bat` bỏ cờ `--dev` (không còn gì để cài thêm). Khối `if` rỗng còn sót lại làm cmd.exe báo "( was unexpected at this time" và script chết ngay sau bước cài thư viện — đã gỡ hẳn. 859 test xanh · 4/4 cổng CASAN · check_design_parity 32/32 · check_layout_geometry trùng từng byte với bản trước refactor. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>