# FR-067 檢測多 Agent 分派 — LOG（append-only）

> **只追加、不改寫既有 block。** 每棒一個 block：commits／決策／教訓／推翻了什麼。
> 現況看同目錄 `FR-067-STATE.md`。

---

## 第一棒 — FR-067.1 地基（2026-08-23）

**卡**：子需求 CM-1351｜T-1.1 CM-1357｜T-1.2 CM-1358
**commits**：`9f83973b` feat(FR-067.1): Agent 清單補能力與在線狀態＋修好自動挑機
**改動**：13 檔（BE code 8＋test 4＋spec 1），未動 DB、未碰 STG／POC

### 決策

**D-1.a 新增 `get_dispatchable_agents()`，而非改掉既有 `get_agents_with_capability()`**
派工要濾離線、清單不能濾離線——同一支方法答不了兩個問題。若直接把在線條件加進既有
方法，清單頁會把離線機藏起來，使用者找不到機器又看不出原因（正是 memory
`feedback_verify_data_source_before_hiding_empty_display` 講的失敗模式）。故拆兩支、
docstring 互相交代分工。既有方法保留原語意，零回歸。

**D-1.b 修在 domain service，不在兩個 caller 各修一次**
盤點發現 `_pick_agent` 與 `test_connection` **早已共用** `get_agents_with_capability`
（FR-056.5 就抽好了）。卡片文字看起來像要改兩處，實際只需換掉共用的那一支，兩邊
自動對齊——這正是 D9③「測通的就是會執行的」最省的達成方式。**沒有另寫第二套挑機邏輯。**

**D-1.c 排序鍵選 `id`**
`uid` 是隨機 UUID、`name` 使用者可改，兩者都不穩定；`id` 單調不變。穩定性有反向
斷言守（輸入順序反轉，輸出不變）。

**D-1.d `is_online`／`utc_now` 搬到 `common/util/agent_auth/heartbeat.py`**
domain 層要做在線判定，但原本這兩支住在 app 層，domain 不得反向依賴 app（DDD 層級
規範）。搬到 common 並在原處 re-export，既有 import（含 3 支測試檔）零改動。

**D-1.e 在線欄位命名 `is_online`（清單）vs `online`（管理頁）並存**
管理頁 `RemoteAgentDetailResponse.online` 是 FE `RemoteAgentManage.vue` 已在用的既有
契約，動它要連 FE 一起改且不屬本卡範圍；新欄位依 design.md §5.6 契約命名 `is_online`。
兩者語意與門檻相同，已在 serializer docstring 交代，避免後人以為是不一致。

### 教訓

**① 「兩處各寫一套」的卡，先查是不是早就共用了**
卡片描述兩個檔案座標，容易直覺開兩處修。實際 grep 後發現 FR-056.5 已收斂到 domain
service，只需換一支。**先 grep 行為再動手**省掉一次重複造輪子（CLAUDE.md「新增
method 前先找既有能力」鐵則的正面案例）。

**② 環境設定值可能與設計文件舉例不同，別把文件數字寫死**
design.md 通篇寫「900s＝300×3」，但 DEV `.env` 的 `AGENT_HEARTBEAT_INTERVAL_SEC=60`
→ 實際門檻 180s。卡片要求「沿用既有常數不自創」正是為此。驗收時若拿 900 去對會誤判。

**③ 同目錄可能有平行 session，commit 前必須逐檔認領**
本棒 commit 前 `git status` 出現 27 個我沒動過的檔（FR-067.2 的 group／assignment 表）。
用 `ls -lT` 比對修改時間發現是**當下正在寫入**的平行 session 產物。若當時用了
`git add -A`，會把對方未完成的半成品掃進我的 commit。**判定方法**：比對檔案 mtime
與自己的作業時間窗、隔一段時間再看 `git status` 行數是否還在變。已寫進 STATE 警示。

**④ 改了共用 canonical，要順手改掉 mock 它的測試——否則測試綠得沒有意義**
四支測試檔 mock 的是舊方法名。不改的話 `MagicMock` 會對新方法名回傳 Mock 物件，
測試可能仍「綠」但驗的是空氣（其中 `test_no_available_agent_raises_bad_request`
原本就會因此變成假通過）。

### 沒做什麼（避免後人誤以為漏了）

- **未寫新 unit test**：依 CLAUDE.md 測試政策，碎片卡預設不寫；卡上未明示要求。
  驗證改走「跑既有相關測試（460 passed）＋實跑 DEV API ＋反向／邊界斷言腳本」。
- **未動 FE**：任務設置的 Agent 下拉屬 FR-067.3 範圍，本棒只交付 BE 讀取面。
- **管理頁仍不顯示 capabilities**：屬 UI 範圍，SPEC 坑 10 已更新為「API 讀得到、
  本頁 UI 未呈現」，未擅自擴大到改管理頁。

---

## 第二棒 — 2026-08-23 — runner（FR-067.2 資料層：CM-1359 T-2.1 migration ＋ CM-1360 T-2.2 DDD 四層）

> 與第一棒（.1 地基）**平行**進行，非接續。兩棒各自 append 自己的 block。

**commits**（BE，皆未 push）：`9bcd02f5`（T-2.1 migration）／`69e6906d`（T-2.2 DDD 四層＋彙總函式，28 檔）
**Notion**: CM-1359／CM-1360 皆回寫「修正待驗證」＋白話已完成修正段＋commit hash；子需求卡 CM-1352 維持 In progress 待驗收。

### 做了什麼

- **T-2.1**：`scripts/sql/2026-08-23-fr067-2-detection-multi-agent-tables.sql`——兩張新表
  ＋`detection_executions` 擴 `group_uid`／`assignment_uid`＋補三支索引（缺口 11 還債）。
  **只套 DEV**，`schema_migrations`＋`manifest.tsv` 皆已登記，**不做 backfill（D8）**。
- **T-2.2**：兩張新表 DDD 四層全套＋DI wiring；`detection_executions` 側同步擴欄並加
  `list_by_group`／`list_latest_per_assignment`；彙總計算獨立成模組級函式。
- **驗證**：30 個新單元測試全綠；彙總函式三輪突變測試；鄰近既有 44 測試不壞；
  DEV 原生 SQL 實跑（`BEGIN…ROLLBACK`）驗 CRUD／`DISTINCT ON`／唯一約束／軟刪加回／列鎖，無殘留；
  ddd-compliance-reviewer 掃過 0 findings。

### 決策（含被排除的）

- **彙總函式擺哪**：選**模組級函式**（`detection_group_status.py`）而非 group domain service
  私有方法。理由：收口、單組重跑、取消、列表呈現四個入口都要算同一件事，私有方法會誘使
  各入口複製一份成第二套真相。group domain service 保留 `compute_status()` 薄轉呼叫當
  domain 入口，不重寫邏輯。
- **`reopen()` 另立一支而不走 `update(entity)`**：`repo.update()` 是「value is not None
  才寫」語意，`closed_at=None` 會被靜默 skip，舊收口時間清不掉——這是幽靈覆寫防護的反面
  代價。故 repo 內另開 `reopen()` 直接操作 model 欄位。**被排除**：entity 加
  `clear_closed_at` 布林旗標（把 repo 寫入語意漏進 entity，更難懂）。
- **assignment 唯一約束用 partial unique index**（`WHERE is_delete = FALSE`）：table-level
  UNIQUE 會讓軟刪列繼續佔 slot，「移除某台再加回同一台」會撞 UniqueViolation。這個坑
  `uq_jedt_job_active` 當年踩過，其 migration 註解留有前例記載，直接沿用。
- **兩張新表 RLS 抄哪一版**：抄 `2026-07-26-fr056-2-jedt-fix-rls-delimiter.sql`（修正後：
  slash-delimiter ＋ `is_super_admin = 't'`），**不是** `job_execution_detection_tools`
  原始建表 migration（逗號 delimiter ＋ `'true'`，該版是壞的，當年 FE 手測才炸出來）。
  新表照抄原始版就會複製同一個 bug。
- **未知 status 的歸類**：`compute_group_status` 把任何非 `succeeded`／`failed`／`cancelled`
  的值都當未終態（回 running），而不是丟進 `partial_failed`。理由：保守——寧可不收口讓
  人工看，也不要誤判成 succeeded 而觸發 D5 auto 自動完成任務。

### 教訓

- **`TenantScopedMixinModel` 讓「獨立 script import model 做 DB smoke」行不通**：該 mixin
  只在 `ENABLE_MULTI_TENANT=true` 時才產出 `tenant_id`／`org_unit_id`，且帶
  `ForeignKey("tenants.id")`——FK 目標表要整個 model registry 載入才解析得到，否則炸
  `NoReferencedTableError`。資料層實跑驗證改用**原生 SQL 打 DEV（包在 `BEGIN…ROLLBACK` 內）**
  反而更快更乾淨，且驗的正是與 repo 相同的 SQL 形狀（`DISTINCT ON` 那段是直接把
  repo 產出的 SQL 抄過去跑）。
- **平行 session 下差點覆蓋對方的交接檔**：本棒寫 STATE／LOG 時，第一棒的同名檔案在
  幾分鐘前才 commit（`75a0a3f5`），本棒是用 `cat >` 整檔覆寫——working tree 內對方版本
  被蓋掉。發現後以 `git checkout <commit> -- <path>` 復原對方版本，再把自己的內容以
  Edit 併入（STATE 併節、LOG append block）。**教訓：交接雙檔在平行作業下要當共享檔對待，
  一律先 `git log --oneline -- <path>` 看有無新 commit，再用 Edit 併入，不要 `cat >` 整檔寫。**

### 推翻了什麼

無。D1–D9 皆照 design.md 實作，無偏離。

### 沒做什麼（避免後人誤以為漏了）

- **未動 orchestration**（`start_execution` 展開、收口、重跑、取消）：那是 .4 範圍，
  本棒只交付資料層能力。`detection_orchestration_service.py` 一行未改。
- **未動 API／serializer／FE**：綁定設置的 `agent_assignments` 欄位屬 .3。
- **未套 STG／POC**：開發鐵律，等決策者放行。
- **未做存量 backfill**：D8 定案（開發驗證階段資料之後重置）。既有 `detection_executions`
  的 `group_uid` 全為 NULL＝legacy 筆，讀取端視為單筆獨立顯示。

---

## 第四棒 — 2026-08-23 — runner（FR-067.4 執行展開與收口：CM-1363 T-4.1 ／ CM-1364 T-4.2 ／ CM-1365 T-4.3）

> 與 .3（綁定設置）**平行**進行。本棒範圍限
> `detection_orchestration_service.py` ／ `remote_agent_domain_service.py` ／
> detection_tools route＋DI＋error code ／ `task_execution_query.py`，**未碰
> `job_service.py` 的 binding 寫入與 FE**。

**commit**（BE，未 push）：`e6283532`（11 檔：BE code 7＋test 4）
**Notion**：CM-1363／CM-1364／CM-1365 皆回寫「修正待驗證」＋白話段＋commit hash；
子需求卡 CM-1354 同步「修正待驗證」。
**DB**：無異動（表在 .2 就建好）。

### 做了什麼

- **T-4.1**：`start_execution` 展開成 group＋N 筆 execution＋N 張工單；D3 逐台驗證
  收集全部不合格一次 400；無 assignment 走 fallback 隱含單組（仍建 group）；
  running group 守門與自動派工冪等上移。
- **T-4.2**：`_close_group_if_terminal()` 列鎖收口——彙總落地／auto gate（只認
  succeeded）／通知 hook 點；`complete_job` 冪等防護；409 地雷解除。
- **T-4.3**：三支新端點（單組重跑／單組取消／整組取消 best-effort）＋既有 job 層
  取消端點改成委派整組。
- **驗證**：新增 31 個單元測試全綠；核心斷言過**六輪突變測試**（放寬 auto gate／
  拿掉冪等／關掉 D3 擋下／取消改非 best-effort／拿掉列鎖／拿掉 job 層委派——每項
  都有測試轉紅）；相關 242 個既有測試全綠；BE 實際重啟成功（DI wiring 過）、三支
  新端點打得到（401＝有守門、bogus 路徑 404 證明比對正確）；改過的冪等 SQL 在 DEV
  實跑通過（唯讀）；ddd-compliance-reviewer 掃過 PASS（唯一 Warning 是 FE error-code
  未同步，屬 .5 範圍）。

### 決策（含被排除的）

- **D3 驗證另開 `diagnose_dispatchability()` 而非複用 `get_dispatchable_agents()`**：
  後者只答「有哪些可派」，答不出「這台為什麼不可派」。D3 要求逐台列原因（避免使用者
  修一台再撞下一台），故條件逐項檢查而非一次過濾掉。**被排除**：用集合差集推斷原因
  ——推不出「同時 disabled 又 offline」這種多重原因，訊息會失真。代價是兩支的資格
  條件要同步維護，已在雙方 docstring 互相交代。
- **fallback 隱含單組也建 group**：本可讓「沒設 assignment」走完全不建 group 的舊路徑，
  但那會讓收口有兩套（有 group 的走新邏輯、沒有的走舊邏輯），auto 完成與通知都要
  分岔判斷。統一建 group 後，`assignment_uid=NULL` 自成一條分組線（.2 已實跑確認 PG
  的 `DISTINCT ON` 對 NULL 的語意正是要的），收口只有一套。
- **通知本體不動、只留 hook**：卡片明示彙總通知是 T-5.2。若在本棒順手改成收口一封，
  會與 .5 撞範圍且 `_notify_scan_result` 的九項內容測試（`TestScanNotificationContent`
  共 7 例）要一起改造。現階段仍是單筆各發一封（行為與 .4 前相同），收口只寫 log。
- **`_auto_dispatch_detection_scans` 冪等條件「加」而非「換」**：卡片寫「改『該任務無
  任何 group』」，但直接換掉會讓 D8 不 backfill 的舊資料（有 execution、無 group）被
  判成「沒派過」而重派一輪。改成兩條並存（無 execution **且** 無 group）。**這是對
  卡片指令的偏離**，理由是照做會製造重複派工。
- **既有 job 層取消端點改成委派整組**（卡片未列）：群組化後不改的話，FE 抽屜的取消鈕
  只會取消 N 台裡的第一台，其餘幾台繼續掃、群組永遠 running 擋住下一輪執行——這是
  群組化**引入**的迴歸，不是既有 bug，屬本棒責任範圍。
- **force 重派仍是「取消失敗即 409」，不採 best-effort**：與 T-4.3 整組取消刻意不同。
  那裡使用者意圖就是收掉這一輪、能收多少算多少；這裡是「確保沒有殘留執行中工作」的
  前置，沒收乾淨就不該派新的（否則掃描器上會疊出並行工作）。

### 教訓

- **「response 形狀改了」要主動 grep FE 有沒有讀**：`start_execution` 從
  `{agent_task_uid, execution_uid}` 改成群組形狀。grep `JobExecutionDrawer.vue` 確認
  FE 只看呼叫成功與否、不讀 body，故無破壞。**沒 grep 就改的話，破壞會等到 .5 串 FE
  才浮現，屆時查起來像是 .5 自己弄壞的。**
- **群組化會「引入」新的迴歸，不只是「新增功能」**：既有 job 層取消端點原本正確，
  正是因為引入群組才變成半殘（只取消第一筆）。做結構性改動時要問「哪些原本對的東西
  會因為這個改動變成錯的」，而不只是「我要新增什麼」。
- **測試綠一次過時反而要更用力驗**：31 個測試第一次跑就全綠，照突變測試紀律故意寫錯
  六處確認每處都有測試轉紅。其中「拿掉列鎖」一輪第一次執行時因為 shell heredoc 的
  字串替換沒生效（`replace` 沒命中），出現「看起來沒轉紅」的假象——**改用檔案比對
  確認替換真的發生**後才看到正確結果。突變測試本身也要驗突變真的注入了。
- **DEV agent 全部離線，展開路徑的成功分支在 DEV 驗不了**：D3 會正確擋下（那是對的
  行為），但也意味著「兩組成功展開」這個主場景無法在 DEV 手測，只有單元測試守著。
  已寫進 STATE 環境注意，避免驗收者以為是 bug。

### 推翻了什麼

- **卡片 T-4.1 的「`_auto_dispatch_detection_scans` 冪等條件改『該任務無任何 group』」**
  ——改成「加一條」而非「換掉」（理由見上方決策段）。這是本棒唯一對派工單指令的偏離。

### 沒做什麼（避免後人誤以為漏了）

- **未改 `receive_result` 不吞例外的既有行為**（卡片明示不必改）。
- **未動彙總通知本體**（`_notify_scan_result` 仍每筆一封）——T-5.2 範圍，收口路徑
  ③ 已留 hook 點。
- **未動 FE、未動 `job_service.py` binding 寫入**（.3 平行 session 的範圍）。
- **未同步 FE `error-code.json`**（5 個新碼）——.4 範圍不含 FE，已寫進 STATE 下一步。
- **未跑全量 pytest**（依測試政策），只跑相關檔＋新檔。
- **未套 STG／POC**（本棒根本無 DB 異動）。

---

## 第三棒 — 2026-08-24 — runner（FR-067.3 綁定設置：CM-1361 T-3.1 BE diff-sync＋enrich ＋ CM-1362 T-3.2 FE Agent 分派 UI）

> 與第四棒（.4 執行展開與收口）**平行**進行。接手時 .4 已收工並 commit（`e6283532`／`10d6b43b`），
> 故本棒的交接雙檔改動是**併入** .4 的版本（Edit 就地併節，未整檔覆寫）。

**commits**（皆未 push）：BE `f6e27786`（11 檔）／FE `996ed22`（3 檔）。**DB 無異動**（表是 .2 建好的）。
**Notion**：CM-1361／CM-1362 皆回寫「修正待驗證」＋白話已完成修正段＋commit hash＋執行紀錄；
子需求卡 CM-1353 維持 In progress 待驗收。

### 做了什麼

- **T-3.1**：`DetectionToolBindingSchema` 擴 `agent_assignments`；寫入端 diff-sync（增／改／軟刪）
  掛在 `_replace_detection_tool_binding` 的 **update 與 create 兩分支**；讀取端每列 enrich
  `agent_name`／`agent_online`（app service 層批次查，不在 infra JOIN）；新增兩個 error code。
- **T-3.2**：`ProjectPlanningView` 檢測工具區新增「Agent 分派」列表（agent 下拉＋掃描目標輸入＋
  增刪列），空列表顯示「自動分派」說明；離線 agent 標示且不可選；已被別列選走的一併反灰；
  i18n 中英雙語補齊。沿用既有 tokens 與全域 class（`hint-bar`／`form-label`），未新增自訂樣式。
- **驗證**：DEV 實打 API 九情境全過（見 STATE 待驗收段）＋綁定 create 分支另驗；control-tree
  端點實跑確認回傳分派與 enrich 名稱；`npm run build:DEV` 通過；ddd-compliance-reviewer 0 findings。
  依測試政策未寫新單元測試（卡上未要求）。

### 決策（含被排除的）

- **`agent_assignments` 的 `None` vs `[]` 分開語意**：`None`＝不動既有、`[]`＝清空。理由：規劃頁
  有「只改任務名稱」的 PUT 路徑，若把未帶欄位當清空，改個名字就會把分派設定洗掉。這與同一
  schema 內 `tool_params`／`completion_mode` 的 None-skip 慣例一致。**被排除**：一律當清空
  （簡單但會誤刪）、一律當不動（則永遠無法清空回 fallback）。
- **配對規則加上 `agent_uid` 當第二鍵**（原本只用 `uid`）：見下方教訓①。**被排除**：要求 FE 必須
  帶 uid——契約上做得到，但「前端漏帶就 500」是脆弱設計，且 `agent_uid` 本來就是 DB 唯一約束的
  欄位，用它配對才與唯一性語意同源。
- **重複 agent 擋在 app service 層回 400，而非讓它撞 DB 唯一約束**：撞約束會丟 500 且訊息是
  psycopg 的英文約束名，使用者看不懂。擋在寫入前也避免部分列已 flush 才炸。
- **FE 一律送出 `agent_assignments`（含空陣列）**：因為空陣列是有意義的值（清空），省略才是
  「不動」。若 FE 用「有值才送」的常見寫法，使用者永遠刪不掉最後一組。

### 教訓

- **① 配對只看 uid 會撞唯一約束（實跑抓到，非推測）**：第一版 diff-sync 用「帶 uid＝更新、
  不帶＝新增」，手測第 2、3 情境立刻 409——因為測試請求模擬 FE 沒帶 uid 的送法，同一台 agent
  走進 create 分支撞 `uq_jedta_binding_agent_active`。**這正是「批次驗證」紀律的價值**：九個
  情境一次跑完，兩個坑在同一輪暴露，修完再驗一輪就收斂；逐項驗會變成兩輪。
- **② repo 的 `add`／`update` 都立即 flush，故 diff-sync 的操作順序有語意**：「先增後刪」在
  「把 A 台換成 B 台」時會撞約束（A 還沒讓位）。**軟刪必須排在新增之前**。這類「同一交易內
  多筆寫入互相踩」的坑，光看 code 想不出來，是靠情境 4（整台換掉）跑出來的。
- **③ 綁定的讀取路徑有兩條，規劃頁走的是不顯眼的那條**：`get_job`／`list_jobs` 走
  `GrcJobRepoImpl._batch_fetch_job_tools`，但**規劃頁任務面板 prefill 實際走 control-tree 的
  raw SQL**（`ssp_control_implementation_service` ＋ `ssp_control_implementation_query`）。
  只補前者的話，存檔當下畫面正常、**重新整理設定就消失**——使用者會回報「存不進去」，實際
  DB 有資料。**任何加在綁定上的新欄位都要同時補這條**。發現方式是實作 FE 時追 `mapAoJobs`
  的資料來源，不是靠 grep 猜。

### 推翻了什麼

無。D2／D8 皆照 design.md 實作；卡片指定的檔案座標與 §5.6 契約皆未偏離。

### 沒做什麼（避免後人誤以為漏了）

- **未碰 `detection_orchestration_service.py`／`detection_result_handler.py`**（.4 平行 session 範圍，
  一行未改）。
- **未同步 FE `error-code.json`**（`GRC_400107`／`GRC_400108`）——與 .4 的 5 個一併留給 .5 串 FE 時補。
- **未寫新單元測試**（依測試政策，卡上未要求），驗證走實打 API ＋ FE build。
- **FE 頁面實際操作未手測**——FE dev server 依慣例由使用者啟動，手測步驟已列進 STATE 待驗收段。
- **未套 STG／POC**（本棒無 DB 異動）。
- **未 push**（等 user 明示）。

---

## 第五棒 — 2026-08-24 — runner（FR-067.5 BE：CM-1366 T-5.1 執行紀錄 group 化 ＋ CM-1367 T-5.2 彙總通知；FR-067.7 BE：CM-1373 T-7.1 排程）

**commit**（未 push）：`0dc088d9`（44 檔，含 2 支新測試檔）。**DB 有異動：只套 DEV**
（`2026-08-24-fr067-7-per-assignment-scheduling.sql`，已入 `schema_migrations` ＋ `manifest.tsv`）。
**Notion**：CM-1366／CM-1367／CM-1373 皆回寫「修正待驗證」；子需求卡 CM-1355／CM-1372 維持待驗收。

三張卡合一個 commit：orchestration service 同時承載執行紀錄列表、收口通知與展開/取消鏈，
拆開 commit 會留下中間跑不起來的狀態。

### 做了什麼

- **T-5.1**：`list_executions` 改回**兩層形狀**（group → executions），每筆補 `agent_uid`／
  `agent_name`／`assignment_uid`／`scan_targets`／`is_latest`；legacy 筆與「群組列查無」
  一律走「單筆自成一組」合成分支（`is_legacy: true`）。順修兩處既有 N+1（缺口 11）：
  scan_params 與 report_file_uid 各補批次版，剝除邏輯抽成 module-level 純函式共用。
- **T-5.2**：收口第 ③ 步接上 `_notify_group_result`（整組一封，每 assignment 一列：
  agent／目標／狀態／發現數／時間／失敗原因），三通道同步；單筆通知**只留給 legacy 無
  group 的筆**。`_dispatch_notification` 的 `error_message` 參數改成 `failed_tone` 布林。
- **T-7.1**：migration 三處加欄（assignment 子表／`agent_tasks` 的 `scheduled_at`、
  `detection_executions.status` 的 `scheduled` 語意）＋領單部分索引；領單查詢加時間過濾；
  綁定 API 讀寫帶 `scheduled_at`；展開時快照進工單與 execution 狀態；取消排程組短路；
  「立即開始」端點；ack 時 scheduled→running。
- **驗證**：322 例相關測試全綠（新增 22＋12 例，順修 .3 遺留三支紅燈 fixture）；六個關鍵
  斷言過突變測試；DEV 實跑執行紀錄 API／排程寫入讀回清除／領單過濾／兩條讀取路徑格式一致；
  彙總通知渲染肉眼確認（純文字＋Email HTML 兩版）；ddd-compliance-reviewer 0 findings。

### 決策（含被排除的）

- **時區契約獨立成 `common/util/scheduled_time.py`（naive 視為 UTC）**：`scheduled_at` 落
  TIMESTAMPTZ，但 jedi-common 的 `db_mw.DATETIME_FIELDS` 只涵蓋 created_at/updated_at，本欄
  **不會**被自動轉時區。FE 若送不帶 offset 的本地時間，psycopg 會以 session TZ（三環境皆
  `Etc/UTC`）解讀 → 設的凌晨兩點變成早上十點才開掃，**且畫面完全正常**（存什麼讀什麼）。
  **被排除**：靠 DB session TZ（隱式、換環境就漂）、在 BE 轉成台北時間（跨時區使用者看到錯的
  牆鐘時間）。
- **領單查詢另開 `list_dispatchable_for_agent`，不改 `list_for_agent_by_status`**：後者是
  FR-058.7（D25）下載授權判定用的，要看 pending/dispatched/running 三態，**不該被排程時間
  過濾**（已領走的單本來就過了時間，套上去 agent 取檔會被自己人擋掉）。兩支分家有測試守著。
  **被排除**：在既有那支加參數——共用一支就得在每個 caller 判斷要不要過濾，是漏判的溫床。
- **execution 的 scheduled 狀態，group 層不另設 scheduled 態**：group 彙總仍為 running
  （`compute_group_status` 視 scheduled 為未終態）。列表的「2 組：1 執行中、1 排程於…」文案
  由 FE 依明細組成。**被排除**：group 加第五個狀態——會讓 D4 彙總表從四態變五態，而收口判定
  只關心「終態了沒」，多一態是純負擔。
- **取消短路是「新增一條分流」而非改既有語意**：既有「可達才標記 cancelled」（best-effort 的
  可達性要求）對已派出的單完全不變；只有「scheduled ＋工單仍 pending」才短路。判定條件**兩個
  都要看**——只看 execution 狀態的話，排程時間到、agent 已領走但尚未回報的瞬間會誤短路，
  掃描還在對方機器上跑卻被標成已取消。
- **發現數空值顯示「—」而非 0**：各 connector 的 summary 鍵不同（OpenSCAP 系 `fail`、Nmap 系
  `findings`），取不到時填 0 會被讀成「掃了但什麼都沒發現」，與「這個工具不回報這個數字」是
  完全不同的意思。
- **通知明細壓成單一列的多行值，而非另立表格**：三通道共用同一份資料，Telegram／Discord 是
  純文字沒有表格可畫，強行雙結構會讓兩版必然漂移（這正是既有 `list of tuple` 設計的理由）。

### 教訓

- **① `_is_unclaimed_scheduled` 用直接取屬性會炸既有測試（實跑抓到）**：測試 fixture 是薄的
  `SimpleNamespace`，沒有 `status` 欄位。改 `getattr(..., None)` 時順手想清楚了**保守方向該往
  哪倒**——判不出來就走「打 agent 取消」（多打一次頂多拿到「查無此任務」），而不是短路
  （該打沒打＝掃描還在跑卻標成已取消）。容錯的預設值本身是設計決策，不是隨手填。
- **② control-tree 那條 raw SQL 直出 datetime 會變 RFC 格式**：`Mon, 24 Aug 2026 18:00:00 GMT`
  vs grc job 路徑的 ISO `2026-08-24T18:00:00+00:00`——同一個欄位兩種格式，FE 得寫兩套解析。
  這條路徑不經 marshmallow schema（本檔 `reviewed_at` 早有 `.isoformat()` 的既有慣例，只是
  沒人在加新欄位時想到要跟）。**加欄位到 control-tree 時，格式一致性要跟資料存在性一起驗。**
- **③ 突變測試抓到一個真漏網**：`is_latest` 與批次化的斷言都有牙齒，但「多筆 legacy 各自成組」
  一開始沒鎖住——把分組鍵從 `f"legacy:{e.uid}"` 改成常數 `"legacy"`（＝所有舊筆擠成一組）
  測試照樣全綠。補了一條測試才抓住。**「跑綠了」不等於「守住了」，突變測試是唯一的檢驗方式。**
- **④ 驗證時改到了 DEV 的真實資料（任務名稱）**：為驗「只改任務名不動分派」的語意，PUT 了
  `{"name": "排程測試任務"}`，把一個真任務改名了。從 `api_logs.response` 撈回原名
  （`GCB Scan use upload profile`）並**走 API 還原**（不是 raw UPDATE——名稱會同步回寫 BPMN
  模板 XML，直接改表會讓兩邊不一致）。**教訓是驗證用的寫入要挑自己建的資料，或先記下原值。**

### 推翻了什麼

無。D6／D10 皆照 design.md §5.6／§5.7／§5.9 實作；T-4.2 留的收口 hook 點 ③ 依約接上。

### 沒做什麼（避免後人誤以為漏了）

- **未做 T-5.3／T-7.2（FE）**——本棒是 BE 面，FE 抽屜 group 化與排程 chip UX 是同一棒的另一半，
  待辦契約已寫進 STATE「FE 待辦契約」節。
- **未同步 FE `error-code.json`**——本棒新增 `DETECTION_TOOLS_409015`，連同 .3／.4 遺留的六個
  共七個，一併列在 STATE 的 FE 待辦契約節。
- **未套 STG／POC**（開發期只動 DEV，等放行）。
- **未 push**（等 user 明示）。
- **未跑全量 pytest**（依測試政策，只跑改動模組的相關檔）。
- **`manifest.tsv` 有兩支既有未登記檔**（`2026-08-18-cm1283-*`／`2026-08-20-cm1317-*`，非本棒
  產生，commit 早於本棒）——`check_migration_manifest.sh` 因此仍 FAIL，**本棒新增的那支已登記**。

---

## 第六棒（.5 FE ＋ .7 FE）— 2026-08-24

**交付**：`JobExecutionDrawer` 群組卡片 ＋ 排程 UX 三端 ＋ 十支 error code i18n。
FE repo `feature/FR-067`：`cb458a6`（群組卡片，CM-1368）／`055d9c2`（設定端排程 chip，
CM-1374）／`80e1c29`（error code ＋ App.vue 明細併陳）。**未 push**。BE repo 無程式異動
（僅本 STATE／LOG）。

T-7.2 的執行紀錄那一端（排程中徽章／立即開始／確認框）落在 `cb458a6` 裡——與 T-5.3 同一個
檔案同一段模板，硬拆成兩支 commit 反而讓任一支單獨看不成立。

### 決策

- **`executions` 從 ref 改成 computed（攤平 `executionGroups`）**，而不是把既有以「單筆」為
  單位的邏輯全部改寫。sticky 源碼檔追溯、執行中判定這些都只關心「有哪些筆」，不關心分組；
  留一個攤平視圖讓它們原封不動，改動面從整個檔縮到只有渲染層。
- **徽章看明細組成，不看 `group.status`**：BE 視 scheduled 為未終態，群組層在排程等待期間
  仍是 `running`。但使用者的心智是「機器正在掃」vs「時間還沒到」，兩者同一個顏色會讓人
  以為卡住。故 `groupStatusKind()` 在 group 為 running 時往下看明細：全 scheduled → 中性
  「排程中」，混合（有一台真的在跑）→ 仍算「執行中」（已經有機器在動了），彙總列補講
  「1 執行中、1 排程於 8/25 02:00」。
- **當前列與歷史列共用同一段模板**（`groupRenderRows()` 合成單一清單，只差 `history` 旗標）
  ——兩者內容完全相同，只是暗一階且無動作鈕。分成兩個 v-for 會變成同一段七十行模板維護兩份。
- **`ACTIVE_EXEC_STATUSES = ['running','scheduled']` 在 FE 也集中成一個常數**，對齊 BE 的
  `_ACTIVE_EXECUTION_STATUSES`。前一棒 LOG 已警告「別在各處另寫 `== 'running'` 字面值」，
  FE 同樣適用——漏掉 scheduled 會讓排程中的組取消鈕不出現。
- **`scheduled_at` 一律送出（含 null）**：它是「每次依送來的值覆寫」語意，與整包
  `agent_assignments` 的「未帶欄位＝不動」是兩個層級。移除 chip 要真的清掉排程。

### 教訓

- **① 實跑真實資料抓到 BE 的 `is_latest` 缺陷——這是靠「跑一次真的 API」才看見的**。
  單看契約文件與模擬 payload，`is_latest` 的語意完全合理；但 DEV 既有 10 筆 legacy 跑下去，
  9 筆變成「歷史」而在預設收合下消失。**分組鍵 `(group_uid, assignment_uid)` 對 legacy 筆
  全是 `(NULL, NULL)`，卻又讓每筆各自成組——兩個決策各自合理，湊在一起就矛盾。**
  作法上的收穫：把 FE 的實際 helper 函式抽出來（esbuild 去型別）餵真實 API 回應跑，
  比讀程式碼想像有效得多，成本也只有幾分鐘。
- **② 補 i18n 反而會弄壞錯誤訊息**：`DETECTION_TOOLS_400017` 的 BE msg 尾端帶逐台原因，
  而 `t(code, {default: msg})` **有翻譯時整個蓋掉 default**。交接文件已標「需保留 msg 內容」
  卻沒說怎麼保留——照字面補翻譯就會把明細吞掉，只剩一句空泛的「有 Agent 無法派工」。
  **「補上翻譯」不是無害動作**：對「訊息本身帶動態明細」的碼，補翻譯等於降低資訊量。
- **③ 交接清單會漏**：STATE 列七（實為八）支 error code，實打四支新端點驗路由時發現
  `DETECTION_EXECUTION_404002` 是四顆新按鈕都會撞到的碼卻不在清單上。**清單是前一棒的
  記憶產物，端點實打才是真相**——花兩分鐘打一輪 404 就補到了兩支。
- **④ best-effort 的成功回應不等於成功**：整組取消時部分 agent 打不到，BE 回 200 並把它們
  放在 `failed`。照 200 報「已取消」是謊——那幾台的掃描還在對方機器上跑，只是本地紀錄被
  標成失敗。**凡是 best-effort 端點，FE 都要讀回應內容而不是只看 HTTP 狀態碼。**

### 推翻了什麼

無。§5.9.4 UX 三端與 STATE「FE 待辦契約」皆照做。唯一與交接文件不符處是那節寫「七個
error code」（實為八支，另補兩支漏列），已在 STATE 就地更正。

### 補做：修掉 BE 的卡片分組鍵（`b205e07a`，同棒稍後，user 指示）

FE 發現的缺陷改在源頭修掉，不只靠 FE 保底。**修法上的重點是「不要選一支鍵去改」**——
`_group_executions()`（分卡）與 `_latest_execution_uids()`（判最新）本來就是同一個分組概念
的兩面，兩處各寫一份鍵才是病根；抽成 `_execution_card_key()` 共用，並在兩支 docstring
互相標註「必須同鍵」的義務，才是讓它不再漂移的作法。真群組行為零變化，僅 legacy 路徑受影響。

回歸測試斷言刻意寫成**性質**（「每張卡至少有一列 `is_latest`」）而非特定 uid 的期望值——
分卡規則日後再變，這條仍然守得住。依 [[feedback_mutation_test_proves_assertion_has_teeth]]
做了突變測試：鍵改回舊版該測試立刻紅、還原後綠。

修後重啟 BE、以 DEV 真實資料重打 API 確認 10 張卡各有 1 列當前（修前 1 有 9 無），並回頭用
FE 實際 helper 跑修好的 payload，確認 FE 保底不會因此重複計算（10 列渲染 10 列、無重複）。

### 沒做什麼（避免後人誤以為漏了）

- **未做任何 live 行為驗證**——DEV agent 全離線，多台展開／重跑／取消／排程領單／立即開始
  都只驗到 build 與邏輯層。逐項打折清單見 STATE「FE 已交付與未驗清單」表。此為 user 當次
  裁示（不等 agent 上線，統一留給端到端驗收棒）。
- **未手測頁面實際操作**（FE dev server 依慣例由 user 啟動）。
- **未 push**（等 user 明示）。
- **FE 側未寫新測試**（依測試政策，UI 卡不強制；FE 無對應 test file）——但**BE 補修有寫**
  （`test_every_card_has_at_least_one_current_row`，屬核心共用邏輯的回歸，符合政策條件②）。
- **未動 .6 收尾**（e2e／SPEC 三頁）。

---

## 協調者棒（中繼交接）— 2026-08-24

**本棒角色**：協調者——逐棒驗收 .1–.5＋.7 全部六棒（**全數通過**）、陪跑端到端驗收、
拍板三個驗收回饋修項的修法。無程式 commit（僅本 STATE／LOG）。

### 進度快照

- .1–.5＋.7 六棒完成且協調者逐棒驗收通過，全部 commit **未 push**。
- 剩：端到端驗收（進行中）＋ 三修項開卡派 runner ＋ .6 收尾棒（未派）。
- DEV 環境重大變化：user 刪掉 123 舊 agent，改裝 **0.2.28 交付包（systemd 原生形態，
  非 compose）**——設定檔 `/etc/guidant-agent/agent.env`、程式
  `/opt/guidant-agent/versions/0.2.28`（current symlink）。已註冊成功：
  `remote_agents` id=17 `ubuntu-lab-03`、`https://192.168.50.123:8443`、
  `[file_storage, detection_scan]`、在線。user 正在跑單台端到端場景
  （基本掃描／排程／立即開始／取消／失敗重跑）。

### 裝機踩坑（實錄）

enroll token 頁回的 endpoint 是 `AGENT_CLOUD_ENDPOINT` 原值
`http://10.8.0.6:8000/api/1.0`（10.8.0.6＝user Mac 的 VPN IP，BE 跑本機），agent 端會
自己再補 `/api/1.0` → 打成 `/api/1.0/api/1.0/agents/register` 404。當場修法＝agent.env
的 `CLOUD_ENDPOINT` 拿掉路徑後重啟即註冊成功；根治留給修項 2。

### 三個驗收回饋修項（已拍板修法，待開卡派 runner，皆規模 S）

1. **ZAP／SonarQube 型工具分派列目標欄多餘**（user 發現＋協調者分析）：hosts 鍵對
   ZAP（只讀 `target_url`）／SonarQube（只讀 `repo_url`）無效，UI 卻照樣渲染目標欄。
   修法：按 `param_schema` 是否含 `hosts` 切——hosts 型（openvas/openscap/inspec/gcb/nmap）
   照現狀，單一目標型分派列只剩 agent 下拉（**用 param_schema 判斷，不寫死工具清單**）；
   BE 展開時對非 hosts 型忽略 `scan_targets` 防禦。
2. **enroll token endpoint 顯示 bug**：`agent_enroll_token_service.py:62` 直接回含
   `/api/1.0` 的原值，使用者照抄必 404。修法二擇一（下一棒定）：BE 顯示時剝路徑，
   或 agent 端 enroll 正規化容忍帶路徑。
3. **BE legacy `is_latest` 語意**：legacy 筆（`assignment_uid` 全 null）被算成同一條
   latest 線，10 筆只有 1 筆 true（FE 保底已擋住消失問題）。該修成 legacy 每筆自成一組
   各自 true。影響低（D8 資料會重置）。

ZAP 項**卡已開**（卡號見 Notion FR-067 母案 CM-1350 下最新子卡）；後兩項可併一張
「驗收回饋修正」卡或掛進 .6。

### 教訓

- **① agent 交付包有兩種形態（systemd 原生／compose）**——排錯前先
  `systemctl list-units` 確認形態，別假設 compose。假設錯形態會去找不存在的容器與
  compose 檔，全程撲空。
- **② endpoint 類設定「含不含路徑」的契約要兩端對齊**：token 頁顯示什麼、使用者就抄
  什麼——顯示端回原值＋接收端自動補路徑，兩個「各自合理」的行為湊在一起就是 404。
- **③ FE 用真實資料實跑抓到 BE `is_latest` 語意問題**——「單元測試綠」不等於
  「真資料形狀對」。真實存量資料（legacy NULL 欄位）是契約文件照不到的角落。

### 推翻了什麼

無。前六棒交付與決策皆維持；`b205e07a` 修的卡片分組鍵與修項 3 的 `is_latest` 語意是
同一區塊的兩個獨立問題（前者已修、後者待開卡），不是推翻。

### 沒做什麼（避免後人誤以為漏了）

- **未開修項 2／3 的卡**（等下一棒協調者，見 STATE「下一步」）。
- **未派 .6 收尾棒**、未做全案收尾（母卡收 Done／SPEC／手冊／analysis 等 user 下令）。
- **未 push**（BE／FE 皆是，等 user 明示）。

---

## 協調者棒（端到端驗收第一輪）— 2026-08-24

**本棒角色**：協調者——收 user 端到端驗收回饋、開六張卡、派四棒 runner（皆開 case 給新
session）、逐棒抽查、user 驗後收 Done。本體 commit 僅 STATE／LOG。

### 進度快照

- **端到端驗收第一輪（雙 agent 雙網段）完成**，六張回饋卡全清：
  - **CM-1375**（Done）：分派列 UX 累積五波——①ZAP/SonarQube 不渲染目標欄（FE `9809d29`
    ＋BE `c408814f`）②掃描目標單一入口＋Agent 分派上移（`8633ed6`）③預設備妥一組
    （`fe10773`）④profile 純下拉（BE `5ec85bee`）⑤重跑歷史收合 v-show 被 PrimeFlex
    !important 壓過（`3a9a213`）＋排程小日曆 minDate（`d10ab5a`）。第⑤波實際收合行為
    留待多列場景複驗。
  - **CM-1377**（Done）：測試連線可指定 agent（BE `06e98031`／FE `4081e58`）——
    `agents[0]` 固定取首台在多 agent 下與實際執行脫鉤。
  - **CM-1378**（Done）：stderr 帶出（`_exec_with_cancel` 原「排空即丟」）＋Rocky 檔名
    推 `rl`（evidence-agent `6fdd541`，bump 0.2.29 `ae9f4c9`）。runner 順抓上層彙整
    「從頭截 300 字」會切掉尾端原因的連帶坑。**0.2.29 尚未部署到兩台 agent。**
  - **CM-1379**（Done）：分派列覆寫 profile＋同 agent 多列（BE `8c24e212`／FE `83248dc`
    ＋migration 拿 `uq_jedta_binding_agent_active`，DEV＋188 stack 已套）。
  - **CM-1380**（Done）：188 驗收 stack 重布最新版＋補 migration。
- 開而未派：**CM-1376**（re-enroll 指令，user 裁示 FR 測完再做）。
- 188 環境：容器 agent（guidant-ai-agent）token 失效 401 循環，經 user 指示已
  `docker stop`（unless-stopped，不自啟）；在線 agent＝id=9（60.166）＋id=11（50.123）。

### 決策

- **CM-1379 判準**：重複擋「(agent, scan_targets 內容) 完全相同」由 BE 驗證層做，不做
  JSONB DB 唯一（鍵序正規化成本高於收益，重複屬輸入錯誤非完整性風險）。
- **content_path 也開放覆寫**，收進階切換、已填自動展開（兩列外觀一樣掃不同基準是雷）。
- **1378／1379 平行派**（不同 repo 零交集）；agent build 與 BE image build 不同主機
  即可並行（派工 prompt 均帶 pgrep 前置檢查）。
- **OpenSCAP content 平台派發**定調為正解方向（沿 FR-059 file 型派發擴到 openscap），
  逐台手放僅過渡——50 台客戶場景逐台維運不成立。

### 教訓

- **① 我在卡上寫的「BE 展開零改動」被 runner 推翻**——profile 參照展開
  （`profile:<uid>` → `_profile` 凍結快照）原在合併**前**，分派列覆寫後第二列會拿
  A 基準檔跑 B 設定且被自家下載授權閘擋，**靜默掃錯基準不報錯**。runner 查證後改為
  「先合併再展開」並加不變量測試。協調者拍板的「零改動」假設也要 runner 驗——寫卡時
  該標「假設，實作前查證」。
- **② OpenSCAP 的 content 生態比設計假設碎**：D3「apt 裝得到」只對 RHEL 系成立——
  Ubuntu 24.04 apt 倉 content 停在 0.1.71（無 2404）、26.04 只有上游 nightly、Rocky
  官方檔名前綴是 `rl` 不是 os-release 的 `rocky`。一輪驗收四台目標三種情況。
- **③ stderr 丟棄讓故障診斷成本爆炸**：60.162 兩次失敗（未裝 oscap／content 檔不存在）
  UI 全是空訊息，都要上目標機手動重現才定位。「錯誤訊息帶 stderr」不是 nice-to-have。
- **④ 檢測工具「測試連線」的單 agent 假設**：FR-067.1 修的「測通的那台＝實際執行的
  那台」在多 agent 下需要「使用者指定哪台去測」才閉環——同類單 agent 假設值得在 .6
  收尾時整體掃一輪。

### 推翻了什麼

- 卡 CM-1379 的「BE 展開零改動」假設（見教訓①，runner 修正）。
- STATE 原「三修項皆規模 S」——修項 1 實際長成 CM-1375 五波＋CM-1379 M 級功能。

### 沒做什麼（避免後人誤以為漏了）

- **未 push**（BE／FE／evidence-agent 皆是，等 user 明示）。
- **agent 0.2.29 未部署**到 60.166／50.123（build 完等 user 指示）。
- **未開卡**：enroll endpoint 顯示／legacy is_latest／content 平台派發。
- **未派**：CM-1376、.6 收尾棒。
- 兩台 U26.04 目標機未配置（engine 可 apt、content 需 nightly）。

### 補記（同棒收尾前，2026-08-24 晚）：Windows 無域憑證議題入列

user 提出：Linux 共用帳號模式在 Windows 只對「有 AD」環境成立；無域（workgroup）
機器同名帳號互不相干、逐台建帳＋密碼同步不可維運，且有 UAC token filter 限制。
協調者分析與三層建議（短期手冊＋前置腳本／中期 per-分派列憑證組覆寫復用 1379 地基／
長期 PAM）已寫進 STATE 待辦第 5 項，**下個 session 首先討論**。此為分析記錄非定案。

---

## 協調者棒（驗收續輪＋.8 每列自足）— 2026-08-25

**本棒角色**：協調者——收續輪回饋開卡派工抽查、主導 FR-067.8 設計拍板與三棒落地、
FR-068 設計定案、環境救火（ZAP）。本體 commit 僅 docs（含一次誤掃事故，見教訓）。

### 進度快照

- **驗收續輪四卡**：CM-1376（re-enroll，0.2.30）／1381（文件一輪）／1382（transport
  per 列＋摘要展開卡）／1383（IP CIDR/range，依**欄位語意**分流展開）——皆抽查過、
  修正待驗證。部署 CM-1384（188＋兩台 agent 0.2.30）／1394（含 .8 再重布）已 Done。
- **FR-067.8 每列自足模型**（user 拍板：廢兩層覆寫改每列完整獨立設定；secret 跟列走；
  零列 fallback 廢掉）：design.md §5.10＋CM-1385~1388 四卡，三棒（BE 列 params 完整化
  `5ca869e7` 內＋FE 整份 param_schema `da33373`＋SonarQube upload per 列 `fda3921b`/
  `a4c5ab4`）全完成抽查通過。文件二輪同步 CM-1393（`a4f063bb`）。
- **FR-068 系統 Log 轉發**設計定案（D1–D6：syslog+GELF 雙協定／app log＋稽核事件
  兩流／系統層預留租戶欄／Queue 旁路／遮罩預留／熱生效）＋四卡 CM-1389~1392 就緒，
  **user 裁示等 FR-067 收完再派**。
- 在飛 CM-1395/1396（顯示回饋）；CM-1397（重跑放寬）已開卡等 1396 收。

### 決策（user 拍板）

- **push 授權放寬（僅限本 arc）**：協調者抽查通過即可 push（runner 或協調者皆可），
  main／STG／POC 照舊等令。memory `feedback_fr067_push_delegated_after_review`。
- **不進版**：v1.15.0 已於 08-19 發過（FR-063/064/065）；FR-067 掛 1.15 滾動演進，
  **v1.16.0 等 PM 確認**（屆時含 FR-066 27 commits）。CM-1394 原進版指令作廢。
- **.8 設計**：每列自足＋複製列＋新列帶第一列預設值；「重用靠 profile 庫引用」對齊
  業界（policy/scanner/job 三分）；不做跨任務參數組庫（反悔條件：客戶抱怨重填）。
- **後移**：憑證多組化（含 Windows 無域）／掃描設定下放指派人員（等 PM）／ZAP 排隊。

### 教訓

- **① 協調者 commit 掃走平行 runner 的 staged 檔**（`5ca869e7`）：顯式 add 自己的檔
  擋不住「別人已 stage 的一起進 commit」——同目錄有 runner 時，commit 前必看
  `git status` 的 staged 區，有別人的東西就停。歸屬靠 Notion 卡標注收拾，不 rebase
  （force push 會弄壞部署 runner 的 checkout）。
- **② 協調者的技術斷言連續兩次被 runner 實查推翻**：1383 卡寫「Nmap hosts 透傳」
  實為執行主機要展開（照卡做整段變一筆連線失敗）；1394 卡寫「bump 1.15.0」實則已
  發過（照卡做會覆寫凍結快照）。**開卡時的環境／版本斷言要先查現況**，或明標
  「假設，實作前查證」——runner 的煞車（停下回報而非硬做）是這兩次的救命機制。
- **③ 省 context 派 sonnet subagent 又陣亡一次**（CM-1381 文件棒 prompt too long）
  ——[[feedback_subagent_use_1m_context_model]] 再驗證；文件同步棒材料量必超小 model。
- **④ 派工 prompt 重工**（連四卡把卡片內容重抄進 prompt）：user 糾正後回歸薄 prompt
  ＝卡號＋讀序＋紀律；任務知識寫卡不寫 prompt。skill Step 5 已補紅字。
- **⑤ ZAP 拿去掃 SPA 會自噬**：ajaxSpider 爬 Next.js 站灌爆佇列，PassiveScan 掃一個
  CSS 要 30 秒、API 逾時、整台 load 30 連 OpenVAS 陪葬。已重啟＋上 6g/2cpu 容器限制
  （docker update，持久）。**ajaxSpider 全域互斥＝同 daemon 掃描必序列**，並發直接
  失敗不排隊。8091 是 proxy 口——健檢要用 `-x` proxy 姿勢打，直接 GET 永遠 refused。
- **⑥ runner 鐵律生效前的空窗**：T-8.2 做完停在 working tree 沒 commit 沒回卡——
  「做完就 commit＋回寫」入 CLAUDE.md 後（cb9601a7）後續棒未再發生。

### 推翻了什麼

- CM-1394 卡的進版指令（協調者自己開錯，runner 查證推翻，已改純部署卡）。
- 1383 卡的 Nmap 分流假設（runner 改依欄位語意分流）。
- T-8.1 的「內部欄位任務層永遠贏」規則被 T-8.3 改為「列上有就列上贏」（已記
  design.md §5.10，防後人改回去）。

### 沒做什麼（避免後人誤以為漏了）

- CM-1395/1396 未收（在飛）；1397 未派（等 1396）。
- `571b5b9e`（1395 的 BE 半）未抽查未 push。
- Windows 無域憑證討論（交接指定首議項）**整棒沒開議**——user 裁示「憑證往後放，
  先共用帳號模式」，短中期方案材料仍在待做第 5 項。
- .6 收尾棒未派；FR-068 未派（user 裁示排 FR-067 後）；全案收尾未動。
- working tree 留兩個未 commit 檔：STATE（本次交接改）＋big-feature-workflow skill
  （薄 prompt 紅字）——隨本交接 commit。

---

## 協調者第四棒（v1.16.0 定版上 POC＋安裝包 P0）— 2026-08-26 ~ 08-27

**本棒角色**：協調者——收 1395/96/97 抽查 push、驗收 11 卡收 Done、主導 v1.16.0
進版全鏈（定版→出包→189 全新安裝）、.6 收尾棒派工、安裝包 P0 事故止血與開卡。

### 進度快照

- **1395/96/97 收官**：三卡批次抽查（開檔驗鎖序/theme token/突變測試紀錄）全過，
  push 後 user 手測收 Done。1397 併發鎖序（lock_for_close 在建紀錄前）是重點驗項。
- **v1.16.0 定版**：CM-1398 進版三件套（RN/bump/快照＋manifest 補登 cm1283/cm1317
  兩支）→ user merge main → tag 三 repo（BE/FE v1.16.0、agent v0.2.30）推遠端。
  發版 gate user 明示豁免。順帶補推 v1.15.0 tag（上次漏推）；v1.10~1.14 遠端原本
  就有（先前「整段空白」是本地視角誤判）。
- **出包＋上 POC**：CM-1399 出包（bundle 891M 烙印 6d4ea5ee／agent onepack 1.3G 含
  seaweedfs／Harbor 六 tag digest 驗過）；CM-1400 189 主產品 user 親裝、CM-1401
  189 LC 更新 user 親做（協調者事後唯讀實機驗證後補記收卡）。
- **.6 收尾棒**：CM-1402 文件半完成（9ae28f68 在 main 未 push）；CM-1403 E2E 半
  runner 額度中斷留半成品。
- **🔴 安裝包 P0（CM-1404 開卡未派）**：189 裝出來缺 8 支 migration，agent 心跳
  500→capabilities 寫不進→檢測功能全殘。已止血（8 支補套＋BE 重啟）。

### 決策（user 拍板）

- **v1.16.0 範圍**＝FR-066＋FR-067（FR-067 branch 已含 FR-066 全部，merge 一次帶齊）；
  FR-068 不進。gate 豁免。簽章照 188 前例用現有鑰（--skip-prod-key-check）。
- **189 全新安裝不遷舊資料**（舊庫遷移涉上傳檔案搬家＋license 重簽，不值）；舊
  postgres 25432 原地封存續跑（license_center_poc 仍用它）。
- **189 LC 更新到 main**（原考慮「共用 188 LC 的照」被 user 否決——LC 太久沒同步，
  要更新）。
- **PROD LC**：正式出版時找一台設備架，現階段不動（懸最久待裁項正式結案）。
- **後續優化池六件**全數定調不開卡（enroll 顯示/legacy is_latest/content 派發/
  Windows 憑證/ZAP 排隊/設定下放）。
- **1402 branch 裁定**：user merge 後停在 main，main 即 current branch，docs commit
  合規；main push 仍留 user。

### 教訓

- **① 出包驗證驗了烙印沒驗 schema 到 head**：CM-1399 驗 bundle 烙印 commit、Harbor
  digest、tar 內容——全過，但 init image 的 02-schema/99-stamp 是 8/18 前 generate
  的 stale 產物，裝出來 DB 缺 8 支 migration。「產物 generate 後 commit 進 repo」
  的機制天生會漂移，build 無 gate 抓不到。修法（gate＋裝機驗 schema）在 CM-1404。
- **② 補套 migration 後 BE 必須重啟**：psycopg cached plan 讓補完欄位後第一次心跳
  仍炸 UndefinedColumn，症狀與「沒套進去」一模一樣——差點誤判補套失敗。
- **③ 查 DB 先確認查的是哪套 PG**：188 有老 25432 與 docker guidant-db 兩套，
  一開始對老 STG 庫算出「缺 12 支」的過期清單，user 糾正後才對到 docker stack。
  「安裝包形態的環境，DB 實況以容器內為準」。
- **④ 檢測工具測試連線 agent 下拉的 >1 顯示條件**會讓「單台/零台租戶」以為功能
  不存在（189 心跳壞掉時 user 即誤判「FE 沒包到」）——設計本身合理，但排錯時
  這是第一個要想到的分支。
- **⑤ zsh 的 for pair in "a b" 不自動斷詞**——批次驗證腳本靜默做錯事（七個 tag
  全對到同一 commit），改用 ${pair%%:*} 拆。

### 推翻了什麼

- 「STG 缺 12 支 migration」的第三棒遺留清單（對老 25432 STG 庫算的，docker stack
  實際已全套；該清單對未來無意義）。
- 「v1.10~1.15 遠端 tag 整段空白」的協調者誤判（僅 v1.15.0 真漏推）。
- 本地 v1.12.0 tag 曾誤打在 6b8023be，與遠端 bed69767 不符，已對齊遠端。

### 沒做什麼（避免後人誤以為漏了）

- CM-1404 未派（P0，接手首件事）；CM-1403 半成品未收；CM-1402 的 main push 未做
  （等 user）。
- 189 使用面未動：.121 agent 服務 inactive、license 未簽新照、租戶未建。
- FR-067 全案收尾未動（母卡/子需求卡/SUMMARY/手冊/memory）；FR-068 未派。
- 188 基線庫（25432 guidant_ai）是否含 cm1283/t52 未查（CM-1404 卡上待查點）。
