"""FR-166 拆卡底表：design.md §6、Notion spec、工作坊四欄都從這張表產，改一處三處一致。

每張卡：id／階段／批／repo／標題／reuse-extend-new／改哪些檔／新開哪些檔／依賴／驗收／model／effort／理由。
批（Stage）＝能同時跑的那一組：不改同一檔、不依賴對方產出、不搶同一外部資源。階段只是分組標籤。
repo 值：main（BE）、fe、jedi-packages、agent、test——agent 是本案新登記（workshop.toml）。
"""

CARDS = [
# ───────── 階段 P0 地基 ─────────
dict(id="P0-1", phase="P0", stage=1, repo="jedi-packages", model="opus", effort="high",
  title="抽 jedi-ldap 零相依套件：目錄連線、首次信任釘住、結構化失敗",
  ren="new（抽出）：從 jedi-iam `ldap_adapter.py` 搬目錄連線段，不是重寫；套件只靠 ldap3",
  edit=[], new=["jedi-ldap/（整支：`LdapDirectory`、`LdapConfig`、`DirectoryUser`、`FingerprintStore` protocol、tests）"],
  deps=[], why="跨套件抽取、要定公開介面與例外形狀；判斷多但範圍小",
  accept="`LdapDirectory(cfg).test_connect()` 對 DEV 的 LDAP 回結構化結果；指紋第一次記、第二次比對、變了回 `CertChanged`；`grep -r flask jedi-ldap/` 為 0；tests 綠且含突變（把比對拿掉要紅）"),
dict(id="P0-4", phase="P0", stage=1, repo="agent", model="opus", effort="medium",
  title="Agent 本機 DB 最小版本機制：schema_version＋有序 migration list（D16）",
  ren="extend：把 `app_factory.py` 的 `create(checkfirst)`＋`_heal_upload_files_columns` 收成 v1，不另寫第二套建表",
  edit=["core/app_factory.py 208"], new=["core/local_db/__init__.py", "core/local_db/migrations.py"],
  deps=[], why="30 行機制但要吃下既有 heal 且不能壞升級；一次做對",
  accept="空 DB 啟動→`schema_version=2`、四張表在；舊 DB（只有 upload_files）啟動→升到 2 且既有列不動；重啟不重套；test 含突變（把 v2 拿掉要紅）"),
dict(id="P0-5", phase="P0", stage=1, repo="jedi-packages", model="opus", effort="high",
  title="jedi-remote-agent 兩張新表、remote_agents 新欄、三個能力點、domain／repo／service",
  ren="extend：照套件既有 domain→repo→service 形狀加兩個 aggregate；能力點進既有 `plugin/contract.py` 清單",
  edit=["jedi_remote_agent/plugin/contract.py", "infra/remote_agent/model/remote_agent.py 87", "domain/.../remote_agent_domain_service.py 171", "migrations/011-remote-agent-offline-operators.sql（新號）"],
  new=["domain/offline_operator/*", "domain/local_run/*", "infra/offline_operator/*", "infra/local_run/*", "app/service/offline_operator_service.py", "app/service/local_run_service.py"],
  deps=[], why="兩個 aggregate、RLS、唯一鍵、能力點契約一次定；形狀照套件既有",
  accept="DEV 套 011 後 `\\d compliance.remote_agent_offline_operators`／`agent_local_runs` 有 RLS 四指令；`test_rls_all_tenant_tables.py` 綠；service 單元測試綠"),
dict(id="P1-7a", phase="P1", stage=1, repo="fe", model="opus", effort="medium",
  title="FE console 殼改側欄＋頂欄＋狀態列、登入頁骨架、選單清單一次寫全（照雛形幕 1～2，先接假 state）",
  ren="extend：`App.vue` 的 phase 機制保留，TabView 換 `Shell.vue`；登入頁從主站 `Login.vue` 複製版面刪分流。**登記型檔集中**：`Shell.vue` 的選單是一份清單常數（九項全寫、view 用 `defineAsyncComponent` 懶載入、檔不存在先指向佔位），之後各分頁卡只新開自己的 view 檔、不碰 `Shell.vue`；i18n `agent-console.json` 各頁的 key 也在這張卡一次佔好（值先放中文占位）",
  edit=["console/App.vue 149", "console/main.js 51", "console/console.scss 91", "i18n agent-console.json"],
  new=["console/Shell.vue", "console/Login.vue", "console/state.js"],
  deps=[], why="純 FE 殼，規格＝雛形；不等 BE 端點（先接 mock state）",
  accept="`npm run build:CONSOLE` 過；開 dist 看到側欄 14rem teal、頂欄、狀態列三態切換；登入頁三分頁依 state 變；`grep border-left console/` 為 0"),

dict(id="P0-2", phase="P0", stage=2, repo="jedi-packages", model="opus", effort="high",
  title="jedi-iam 瘦身接 jedi-ldap：失敗計入鎖定、憑證釘住欄、換主機重填密碼",
  ren="extend：`LdapAdapter` 刪目錄段改呼叫 jedi-ldap，只留綁定 30 行；鎖定計數走既有 `login_failed_count` 路徑，不另開計數器",
  edit=["jedi_iam/.../ldap_adapter.py 295", "login_config_assembler.py 111", "login_service.py 147", "user_auth_provider_service.py 199", "login_error_code.py"],
  new=[], deps=["P0-1"], why="改登入核心、三個資安洞一次補；要突變測試",
  accept="LDAP 密碼錯 5 次→鎖（與本地同一計數）；`LDAP_VERIFY_CERT` 不再被讀；連線 test 換 host 不帶舊密碼；既有 jedi-iam tests 綠＋新增三條突變"),
dict(id="P0-6", phase="P0", stage=2, repo="jedi-packages", model="opus", effort="high",
  title="jedi-remote-agent 心跳契約擴充、終態冪等、local-runs 批次端點、restart 端點、7 天 reaper、事件碼",
  ren="extend：心跳收發照 `heartbeat_console.py` 白名單收法加欄位；`receive_result` 短路條件從 cancelled 擴成任何終態（改既有判斷，不另開路徑）；reaper 照 `run_daily_tick` 既有排程形狀",
  edit=["app/service/agent_enrollment_service.py 608", "app/service/agent_task_service.py 170", "api/routing.py 142", "app/service/heartbeat_console.py", "common/code.py（事件碼）"],
  new=["app/service/heartbeat_offline.py", "app/service/agent_offline_reaper.py", "api/routes/local_run_route.py", "api/routes/agent_restart_route.py"],
  deps=["P0-5"], why="心跳契約是 Agent 與主系統的對外約定，一次定完；冪等改動碰核心",
  accept="同 uid 重送 succeeded 回 200 且 `write_success` 只跑一次（test）；心跳回應含 `offline_operators[]`；`POST /agents/local-runs` 51 筆回 400、重複 `(agent_id,local_uid)` 去重；reaper 對離線 8 天的 dispatched 標 failed、7 天不動"),
dict(id="P1-3", phase="P1", stage=2, repo="agent", model="opus", effort="high",
  title="Agent 離線身分：名單快取 observer、local-admin／離線密碼 argon2id、鎖定、有效期、30 天停用、未同步判定；一次掛三個心跳 observer",
  ren="extend：`local_login.py` 既有 `LOCAL_ADMIN_SUB`＋`_create_local_session` 加密碼路徑，不新身分；session 走既有 `SessionStore` 加 `revoke_by_sub`；observer 照 `_observe_allowed_targets` 形狀。**登記型檔集中**：`enroll.py` 這張卡一次掛 `offline_operators`／`restart_flag`／`outbox.drain` 三個 observer（後兩支先放 no-op 檔），P2-9／P3-1 只填自己的檔、不碰 `enroll.py`",
  edit=["core/enroll.py 411（掛三個 observer）", "core/console/session.py 75", "core/console/local_login.py 188", "core/heartbeat_status.py"],
  new=["core/console/local_auth.py", "core/offline_operators.py", "core/local_credentials.py", "core/restart_flag.py（no-op 殼）", "core/outbox.py（no-op 殼）"],
  deps=["P0-4"], why="認證核心：argon2、鎖定單調時鐘、四種失敗同一回應；必含突變測試",
  accept="`local_auth.verify(sub, pw)` 對名單外／密碼錯／到期／被鎖回同一個結果物件；5 次錯鎖 15 分（monotonic）；撤銷→該 sub session 全消；30 天停用旗標對；未同步判定＝`allowed_targets.received and operators.received`"),
dict(id="P2-4", phase="P2", stage=2, repo="agent", model="opus", effort="high",
  title="網路除錯執行器 core/netdiag/：儀表板讀取、十項診斷、estimate 噪音、判讀規則、報告",
  ren="new（新模組）＋reuse：TLS 用既有 `cloud_trust.describe_tls_failure`、時鐘用 `clock_skew`、目標守門用既有 `allowed_targets.is_allowed()`；PDF 走既有 WeasyPrint 路徑",
  edit=[], new=["core/netdiag/__init__.py", "dashboard.py", "probes/{ping,dns,port,tls,http,ssh,mtu,traceroute,mtr,cloud}.py", "verdict.py", "report.py", "limits.py"],
  deps=[], why="純新模組、無 UI 依賴；但 subprocess 安全、逾時、噪音計算要嚴，判斷多",
  accept="每支 probe `shell=False`、參數陣列、逾時固定（grep 守衛）；`limits.estimate()` 對全勾回 >100 次、等級高；埠 >5 拒；`verdict` 對固定 fixture 回確定結論；`grep CERT_NONE` 為 0；未同步只放 cloud 序列"),

dict(id="P0-3", phase="P0", stage=3, repo="jedi-packages", model="sonnet", effort="medium",
  title="jedi-system-core LDAP 設定組加釘住指紋欄、服務帳密改 Fernet",
  ren="extend：`THIRD_PARTY_LOGIN` 設定組加兩鍵；Fernet 用既有 `jedi_common` 加密 helper（先 grep），不另寫",
  edit=["jedi_system_core 設定 schema", "migrations（套件）"], new=[],
  deps=["P0-2"], why="加欄＋既有 helper，機械",
  accept="設定存取回 `trusted_fingerprint`／`trusted_at`；明文密碼升級後讀回是密文、解得開；既有設定頁不壞"),
dict(id="P0-7", phase="P0", stage=3, repo="fe", model="sonnet", effort="medium",
  title="主站 LDAP 設定頁：拿掉驗證憑證勾選、已信任憑證＋重新信任、預設 STARTTLS（雛形幕 11）",
  ren="extend：改 `LdapConfigForm.vue` 既有欄位，不新開頁；錯誤碼同步 `error-code.json`",
  edit=["src/views/ldap-config/LdapConfigForm.vue 279", "src/config/locales/.../error-code.json", "src/config/api/api.js"], new=[],
  deps=["P0-2"], why="單頁改版、規格＝雛形",
  accept="幕 11 三變化；`grep verify_cert` 為 0；加密下拉預設 STARTTLS、「無」在末尾標僅限測試；憑證變了顯示紅帶＋重新信任按鈕"),
dict(id="P1-1", phase="P1", stage=3, repo="main", model="opus", effort="medium",
  title="主系統接線：plugins/remote_agent 接 P0-5／P0-6 port、三端點能力點、能力點 migration、CIDR 交集驗證",
  ren="extend：`core/plugins/remote_agent.py` 既有 adapters 加 port，新邏輯進 `_agent_offline_operators.py`（照 `_agent_vulndb.py` 先例）；能力點 migration 照 FR-157 `2026-10-22-fr157-agent-console-capabilities.sql` 形狀",
  edit=["core/plugins/remote_agent.py 289", "core/host_capabilities.py 275"], new=["core/plugins/_agent_offline_operators.py", "scripts/sql/2026-xx-xx-fr166-agent-standalone-capabilities.sql"],
  deps=["P0-5", "P0-6"], why="接線＋一支主線 migration；形狀全有先例",
  accept="DEV 套 migration 後三能力點在；`_ADMIN_CAPABILITIES` 三端點登記；CIDR 無交集回 400；BE 重啟實打三端點 200／403"),
dict(id="P1-4", phase="P1", stage=3, repo="agent", model="opus", effort="medium",
  title="Agent 登入 API：local-session、me/password、state；稽核五個新動作；一次登記四支 console blueprint",
  ren="extend：守門用既有 `require_session(scope)`；稽核用既有 `audit.record`＋`REPORTED_ACTIONS` 加五值。**登記型檔集中**：`routes.py` 這張卡一次掛 `local_auth_routes`／`scan_routes`／`netdiag_routes`／`ops_routes`（後三支先放空 blueprint 檔），之後 P2-2／P2-5／P2-8a 只填自己的檔、不碰 `routes.py`",
  edit=["core/console/routes.py 212（掛四支 blueprint）", "core/console/audit.py 138（REPORTED_ACTIONS 加五值）"], new=["core/console/local_auth_routes.py", "core/console/scan_routes.py（空殼）", "core/console/netdiag_routes.py（空殼）", "core/console/ops_routes.py（空殼）"],
  deps=["P1-3"], why="三端點薄層、守門與稽核全 reuse",
  accept="四種失敗同 401 同 body（test）；body >1KB 413；`GET /console/state` 無 session 可打、回四態；改密碼要舊密碼；稽核 jsonl 有 `local_login` 無密碼欄"),
dict(id="P1-5", phase="P1", stage=3, repo="agent", model="opus", effort="medium",
  title="主控台連入 CIDR 白名單：安裝底線、心跳交集、隱含 loopback、連入檢查、CLI reset（開 scripts/install/lib/）",
  ren="new（小模組）＋extend：`browser_plane_server.py` 連入檢查加一處；CLI 子命令進新開 `lib/`（install.sh 超標只准拆），`case` 一行呼叫",
  edit=["core/browser_plane_server.py 218", "config/config.py 162", "scripts/install/install.sh 3426（case 一行）", "deploy/install.sh 2894（case 一行）"],
  new=["core/console_allow_cidr.py", "scripts/install/lib/console_allow_cidr.sh"],
  deps=[], why="連線層守門＋首支 lib 子命令；要定 lib 的載入慣例",
  accept="底線 `192.168.10.0/24` 時 10.0.0.1 連 8444 被拒、127.0.0.1 放行；心跳下發更寬的被忽略＋稽核；`console-allow-cidr reset` 後不限；兩份 install.sh 都只多一行"),
dict(id="P2-1", phase="P2", stage=3, repo="agent", model="opus", effort="high",
  title="Agent 手動執行骨架：_run_one 抽 sink、local_runs 寫入、並發上限、ZAP 網址納清冊",
  ren="extend（抽共用）：`_run_one` 的 ack／result／report-sink 抽成可注入介面，雲端 sink＝現況、本機 sink 新增；**不複製 _run_one**；`allowed_targets.py` 加網址欄位解析（改既有 `_target_networks`）",
  edit=["core/task_executor.py 369（淨減）", "core/allowed_targets.py 203"], new=["core/task_sinks.py", "core/local_runs.py"],
  deps=["P0-4", "P1-3"], why="核心執行路徑重構，兩條路徑共用一支；清冊規則擴充要守衛",
  accept="主系統派工行為不變（既有 tests 綠）；本機 sink 寫 `local_runs`；第 3 個掃描排隊；ZAP `https://x/` 解成清冊外 IP 被拒、清冊內放行；離線 30 天／未同步時 `start()` 拒"),

dict(id="P1-2", phase="P1", stage=4, repo="fe", model="opus", effort="medium",
  title="主站離線操作者授權對話框＋清單欄（雛形幕 10 上半）",
  ren="new（子元件）：`RemoteAgentManage.vue` 766 只加一行掛＋一欄，不往裡加；表單元件 reuse 主站既有 Dialog／DataTable",
  edit=["src/views/remote-agent/RemoteAgentManage.vue 766（一行＋一欄）", "src/config/api/api.js", "i18n"], new=["src/views/remote-agent/OfflineOperatorsDialog.vue"],
  deps=["P1-1"], why="一頁對話框、規格＝雛形；權限反灰邏輯要對 capability",
  accept="幕 10 離線操作者段；沒能力點的格反灰；「Agent 檔案」欄標整台；0 位紅字；撤銷後「有待同步的撤銷」"),
dict(id="P1-6", phase="P1", stage=4, repo="agent", model="opus", effort="high",
  title="安裝與升級流程：新裝強制設 local-admin、升級分互動／無人值守、reset-admin-password／wipe、re-enroll 清本機、upgrade 前問執行中（雛形幕 12）",
  ren="extend：兩份 install.sh 各只在 `case` 加行、安裝題目段加兩題；新子命令進 P1-5 開好的 `lib/`；re-enroll 擴充既有 `do_re_enroll`",
  edit=["scripts/install/install.sh 3426", "deploy/install.sh 2894"], new=["scripts/install/lib/local_admin.sh", "scripts/install/lib/wipe.sh"],
  deps=["P1-3", "P1-5"], why="兩份超標 shell、無人值守分支、密碼寫入要對 P1-3 的雜湊格式；坑多",
  accept="新裝不設密碼裝不完；`upgrade </dev/null` 不問只紅字；`reset-admin-password` 後舊密碼失效；`wipe` 先清 SQLite 再刪目錄；re-enroll 有 outbox 先問；shellcheck 綠"),
dict(id="P1-7b", phase="P1", stage=4, repo="fe", model="opus", effort="medium",
  title="FE console 登入頁接真端點：三態、四種登入、設離線密碼對話框、我的離線密碼、改密碼（雛形幕 1 五變化）",
  ren="extend：接 P1-7a 殼與 P1-4 端點；`api.js` 401 走既有 `onSessionLost`",
  edit=["console/Login.vue", "console/Shell.vue", "console/state.js", "console/api.js 74"], new=["console/SetOfflinePasswordDialog.vue"],
  deps=["P1-7a", "P1-4"], why="接線＋五個畫面變化",
  accept="幕 1 五變化實打：離線密碼登入進總覽；失敗同一句；斷線 30 天只剩本機管理員；跳轉首次自動跳設密碼框；改密碼成功"),
dict(id="P2-2", phase="P2", stage=4, repo="agent", model="opus", effort="high",
  title="手動掃描 API：tools／targets／schema／runs／cancel；第三方元件走既有 scan-input 與 source_scans",
  ren="extend：`scan_input.py` 多接受本機 session（不另開上傳口）；帳密走既有 `credential_resolver`；表單定義走既有 `tool_schemas`",
  edit=["core/console/scan_routes.py（P1-4 已掛的空殼，填內容）", "core/scan_input.py"], new=[],
  deps=["P2-1"], why="六端點、清冊與帳密兩道守門、第三方元件分流",
  accept="`GET tools` 回八支含帳密狀態；`POST runs` 清冊外 403；未同步 403；sbom 走 `PUT /scan-input` 本機 session 成功；取消走既有 `_RUNNING_TASKS`"),
dict(id="P2-5", phase="P2", stage=4, repo="agent", model="opus", effort="medium",
  title="網路除錯 API：dashboard／targets／runs／report；未同步只放 cloud 序列",
  ren="extend：掛既有 blueprint；守門 `require_session('net-diag')`；寫入走 P2-1 的 `local_runs`",
  edit=["core/console/netdiag_routes.py（P1-4 已掛的空殼，填內容）"], new=[],
  deps=["P2-1", "P2-4"], why="薄層接 P2-4",
  accept="`POST runs` 勾全部回 estimate 超門檻需 `confirm=true`；未同步只准 cloud；報告 txt／pdf 下載過 scope；`local_runs` kind=diag"),
dict(id="P2-8a", phase="P2", stage=4, repo="agent", model="opus", effort="high",
  title="維運 API：重啟（優雅結束）、uptime、支援診斷包、儲存設定表單 API（抽 secret_form 與工具帳密共用）",
  ren="extend（抽共用）：`tools.py` 213 的表單儲存段抽成 `secret_form.py`，工具帳密與儲存設定、P4-1 的 LDAP 都用它；重啟掛 `main_app.py` 結束鉤子",
  edit=["main_app.py 31", "core/console/tools.py 213（抽出）", "core/console/ops_routes.py（P1-4 已掛的空殼，填內容；儲存設定路由也放這支）"],
  new=["core/console/ops.py", "core/console/secret_form.py", "core/support_bundle.py"],
  deps=["P1-3"], why="抽共用表單層是本案最重要的反義大利麵動作；重啟要優雅",
  accept="重啟 when_idle 等任務完才 exit 0、now 立即且任務標 cancelled；診斷包 zip 無帳密無清冊；儲存表單四型別存讀測；`tools.py` 行數淨減且既有工具帳密 tests 綠"),

dict(id="P1-8", phase="P1", stage=5, repo="agent", model="sonnet", effort="medium",
  title="總覽 API：狀態燈、這台 Agent 資訊、正在執行（雛形幕 2 三區塊）",
  ren="extend：`overview.py` 33 加三組組裝，燈與資訊分開回；資料來源全 reuse（heartbeat_status、disk_space、local_runs、credential_store）",
  edit=["core/console/overview.py 33"], new=[],
  deps=["P1-3", "P2-1"], why="組裝既有資料、形狀＝雛形",
  accept="回應三區：`lamps[]`（name/state/why）、`info{}`、`running[]`；未同步回 `registration{}` 且其他三區空"),
dict(id="P2-9", phase="P2", stage=5, repo="agent", model="sonnet", effort="low",
  title="心跳旗標重啟 observer：when_idle／now",
  ren="extend：observer 照既有形狀掛 `enroll.py` 一行；重啟動作 reuse P2-8a 的 ops",
  edit=["core/restart_flag.py（P1-3 已掛的 no-op 殼，填內容）"], new=[],
  deps=["P1-3", "P0-6", "P2-8a"], why="一支 observer",
  accept="心跳帶 `restart: now`→任務 cancelled 後 exit；`when_idle`→等；回報 `last_restart_reason`"),
dict(id="P3-1", phase="P3", stage=5, repo="agent", model="opus", effort="high",
  title="Agent outbox：表、入列、心跳後 drain、退避、丟棄順序、離線遲滯、磁碟門檻",
  ren="extend：`_result` 走 P2-1 抽好的 sink，失敗入列；drain 掛 `enroll.py` observer；idempotency_key 配 P0-6 終態冪等",
  edit=["core/outbox.py（P1-3 已掛的 no-op 殼，填內容）", "core/task_sinks.py（雲端 sink 失敗→入列）"], new=[],
  deps=["P0-4", "P1-3", "P2-1"], why="續傳本體：退避、丟棄順序、重送冪等，邊界多",
  accept="斷網跑完任務→outbox 1 筆；復線一次心跳後 0 筆且主系統收到一次；滿 10,000 先丟 audit 再 local_run、task_result 不丟；3 敗切離線 2 成切在線"),
dict(id="P3-2", phase="P3", stage=5, repo="main", model="opus", effort="medium",
  title="主系統收自主執行紀錄：local-runs 端點接線、落庫、報告下載走資料面 JWT、nickname enrich",
  ren="extend：接 P0-6 端點到 plugins；下載 reuse 既有 `data_plane_auth`；enrich 用 `common/util/audit_nickname.py`",
  edit=["core/plugins/remote_agent.py"], new=["core/plugins/_agent_local_runs.py"],
  deps=["P0-6", "P1-1"], why="接線＋既有 helper",
  accept="Agent 送 2 筆→`agent_local_runs` 2 列含 `started_by_name`；重送不增；下載 URL 帶資料面 JWT 能拿到報告"),
dict(id="P3-5", phase="P3", stage=5, repo="main", model="sonnet", effort="low",
  title="7 天 reaper 接線：抽 core/scheduler_agent.py（scheduler.py 714 只准拆）",
  ren="extend（拆檔）：`scheduler.py` 加一行呼叫，邏輯進新檔；reuse P0-6 `run_daily_tick()`",
  edit=["core/scheduler.py 714（一行）"], new=["core/scheduler_agent.py"],
  deps=["P0-6"], why="接一條排程",
  accept="排程具名 `agent_task_offline_reaper`；DEV 造一筆離線 8 天 dispatched→失敗 `agent_unreachable`；0 筆不寫 log"),
dict(id="P3-7", phase="P3", stage=5, repo="agent", model="opus", effort="medium",
  title="守衛測試（Agent 側七條）：入口過清冊、無 shell=True、無 nmap/tcpdump/iperf、local-admin 清冊外 403、未同步 403、無新增使用者路由、無 CERT_NONE",
  ren="new（test）：照 BE `test/test_module_boundaries.py` 形狀寫 grep 型守衛",
  edit=[], new=["test/test_fr166_guards.py"],
  deps=["P2-2", "P2-5"], why="守衛要能抓到真實違規，每條含突變",
  accept="七條綠；每條有「故意違規一次會紅」的突變證明寫在 docstring"),

dict(id="P2-3", phase="P2", stage=6, repo="fe", model="opus", effort="high",
  title="FE console 掃描工具分頁：步驟條、工具卡、清冊目標、tool_schemas 表單、執行中、ZAP 網址、第三方元件上傳（雛形幕 3 四變化）",
  ren="reuse：參數表單用主站 `DetectionConfigField`；步驟條照主站派任務精靈；上傳走既有 scan-input 元件",
  edit=["console/Shell.vue（加選單項）"], new=["console/views/Scan.vue", "console/scanApi.js"],
  deps=["P1-7b", "P2-2"], why="四個畫面變化、表單動態繪製",
  accept="幕 3 四變化實打；手打清冊外 IP 當場擋；ZAP 網址解析顯示；sbom 上傳到這台；執行中進度輪詢、取消"),
dict(id="P2-6", phase="P2", stage=6, repo="fe", model="opus", effort="high",
  title="FE console 網路除錯分頁：資訊區＋連線狀態燈、目標、勾選、噪音計算條、超門檻確認框、路徑圖、判讀卡（雛形幕 4）",
  ren="new（元件）：`PathFigure`／`VerdictCard` 新開；燈元件與總覽共用（P1-8 同形）",
  edit=[], new=["console/views/NetDiag.vue", "console/components/PathFigure.vue", "console/components/VerdictCard.vue", "console/netdiagApi.js"],
  deps=["P1-7b", "P2-5"], why="即時 estimate、確認框、路徑圖繪製",
  accept="勾項目即時打 estimate 更新噪音條；超門檻跳框列原因；確認後結果路徑圖＋判讀卡；匯出 txt／pdf"),
dict(id="P2-7", phase="P2", stage=6, repo="fe", model="opus", effort="medium",
  title="FE console 執行紀錄分頁：同一表、來源欄、回傳三狀態、立即重試、報告下載（雛形幕 5）",
  ren="reuse：主站 DataTable 慣例；重試打 P3-1 的 resend",
  edit=[], new=["console/views/Runs.vue"],
  deps=["P1-7b", "P2-2", "P3-1"], why="一張表＋三狀態",
  accept="幕 5；離線重試反灰；在線重試後變已回傳附時間；拒絕列有"),
dict(id="P2-8b", phase="P2", stage=6, repo="fe", model="opus", effort="medium",
  title="FE console 服務與重啟分頁＋總覽改版：狀態卡、重啟確認框、本機設定、儲存位置表單、診斷包；總覽三區（雛形幕 6、幕 2）",
  ren="reuse：儲存表單與工具帳密表單同一個元件（`Tools.vue` 467 的表單段抽成 `SecretForm.vue`，兩頁共用）",
  edit=["console/views/Overview.vue 141", "console/views/Tools.vue 467（抽出）"], new=["console/views/Ops.vue", "console/components/SecretForm.vue", "console/components/LampGrid.vue"],
  deps=["P1-7b", "P1-8", "P2-8a"], why="抽共用表單元件＋兩頁",
  accept="幕 6 兩變化、幕 2 三態；`Tools.vue` 行數淨減且既有工具帳密頁不壞；燈元件總覽與除錯頁同一支"),
dict(id="P3-3", phase="P3", stage=6, repo="main", model="sonnet", effort="medium",
  title="主站自主執行紀錄頁 BE 側：ui_routes＋route_capabilities migration（FR-132 守衛）",
  ren="extend：照既有 `ui_routes` migration 形狀一支",
  edit=[], new=["scripts/sql/2026-xx-xx-fr166-agent-local-runs-route.sql"],
  deps=["P1-1"], why="一支登記型 migration",
  accept="DEV 套後選單有「Agent 自主執行紀錄」且綁 `read` 或 `net-diag`；FR-132 守衛測試綠"),
dict(id="P3-4a", phase="P3", stage=6, repo="jedi-packages", model="sonnet", effort="low",
  title="jedi-remote-agent `_enrich_online` 補離線等待回傳狀態欄（detection_orchestration_service 只讀不加）",
  ren="extend：在既有 `_enrich_online` 加兩欄；不碰 jedi-detection 超標檔",
  edit=["jedi_remote_agent/.../remote_agent_service.py `_enrich_online`"], new=[],
  deps=["P0-6"], why="加兩欄",
  accept="任務序列化回 `agent_offline_waiting: true`＋`agent_last_heartbeat_at` 當 Agent 離線且任務 dispatched/running"),

dict(id="P3-3b", phase="P3", stage=7, repo="fe", model="opus", effort="medium",
  title="主站自主執行紀錄頁 FE：篩 Agent／種類／工具／人／日期、報告下載、離線標示（雛形幕 10 中段）",
  ren="new（view）：主站 DataTable 慣例；篩選 SegmentButtons",
  edit=["src/config/api/api.js", "i18n", "路由表"], new=["src/views/remote-agent/AgentLocalRunsView.vue"],
  deps=["P3-2", "P3-3"], why="一頁清單",
  accept="幕 10 自主執行紀錄段；Agent 離線那筆顯示「報告在離線的 Agent 上」；下載走資料面"),
dict(id="P3-4b", phase="P3", stage=8, repo="fe", model="sonnet", effort="low",
  title="主站 Agent 清單待傳／LDAP 欄＋要求重啟對話框＋任務頁離線橫幅（雛形幕 10 清單與下半）",
  ren="new（兩個子元件）`RestartAgentDialog.vue`、`AgentOfflineBanner.vue`；`RemoteAgentManage.vue` 766 只加兩欄一顆按鈕，不往裡加邏輯",
  edit=["src/views/remote-agent/RemoteAgentManage.vue（兩欄＋一行按鈕）", "任務頁元件", "i18n"], new=["src/views/remote-agent/RestartAgentDialog.vue", "src/components/remote-agent/AgentOfflineBanner.vue"],
  deps=["P3-4a", "P3-6"], why="三個小 UI 都掛同一支超標檔，併一張避免同批撞檔",
  accept="幕 10 清單三欄、要求重啟段、任務頁顯示段；送出重啟後 Agent 下次心跳重啟"),
dict(id="P3-6", phase="P3", stage=7, repo="main", model="sonnet", effort="low",
  title="主系統要求重啟接線（BE）",
  ren="extend：接 P0-6 端點到 `_ADMIN_CAPABILITIES`",
  edit=["core/plugins/remote_agent.py"], new=[],
  deps=["P0-6", "P1-1", "P2-9"], why="一條接線",
  accept="`POST /remote-agents/{uid}/restart` 回 200 且下次心跳回應帶 `restart`；無 `restart` 能力點 403"),
dict(id="P4-1", phase="P4", stage=7, repo="agent", model="opus", effort="high",
  title="Agent LDAP 三選一：裝 jedi-ldap、ldap_source、同步收心跳 ldap_config、登入多一分支、Nuitka include",
  ren="extend：登入分支加在 P1-3 的 `local_auth.py`；設定表單用 P2-8a 的 `secret_form.py`；指紋存既有 `local_credentials`",
  edit=["core/console/local_auth.py", "scripts/build/build_agent_release.sh", "pyproject.toml（加 jedi-ldap）"], new=["core/ldap_source.py"],
  deps=["P0-1", "P1-3", "P2-8a"], why="三種來源、登入分支、Nuitka 打包；要測名單外 AD 密碼對也拒",
  accept="synced 模式收心跳設定後 AD 密碼登得進；名單外 AD 對仍 401；local 模式自設可連；none 退回離線密碼；Nuitka build 含 ldap3"),

dict(id="P4-2", phase="P4", stage=8, repo="main", model="opus", effort="medium",
  title="主系統 LDAP 設定下發心跳：ldap_config 組裝（服務帳密密文下發、Agent 轉存）、清單 LDAP 欄值",
  ren="extend：照 `_agent_vulndb.py` 先例新開 `_agent_ldap_config.py`；設定讀 P0-3 的 system-core",
  edit=["core/plugins/remote_agent.py"], new=["core/plugins/_agent_ldap_config.py"],
  deps=["P0-3", "P0-6", "P4-1"], why="密文下發方式要定（design §5.5 已定走 mTLS 下發密文、Agent 用自己 key 轉存）",
  accept="心跳回應含 `ldap_config`（密文）；Agent 端轉存後能用；清單 LDAP 欄顯示 synced/local/none"),
dict(id="P4-3", phase="P4", stage=8, repo="fe", model="sonnet", effort="medium",
  title="FE console LDAP 設定分頁：三選一、同步唯讀、自設表單（雛形幕 7）",
  ren="reuse：表單用 P2-8b 的 `SecretForm.vue`",
  edit=[], new=["console/views/LdapSettings.vue"],
  deps=["P2-8b", "P4-1"], why="一頁＋共用表單",
  accept="幕 7；同步模式唯讀；自設測試連線；指紋顯示"),

dict(id="P4-6", phase="P4", stage=9, repo="test", model="sonnet", effort="medium",
  title="e2e：主站 LDAP 登入鎖定與釘住、Agent 管理頁三個新動作",
  ren="reuse：site-regression 既有 page object",
  edit=[], new=["site-regression features/steps（LDAP、agent-manage）"],
  deps=["P0-7", "P1-2", "P3-3b", "P3-4b"], why="照既有套件補 case",
  accept="headless 綠；Agent 主控台不在範圍"),
dict(id="P4-4", phase="P4", stage=9, repo="main", model="sonnet", effort="medium",
  title="SPEC：Agent 主控台頁改版、Agent 管理頁、LDAP 設定頁（writing-feature-specs）",
  ren="extend：改既有 spec 頁",
  edit=["docs/spec-site/current/（三頁）"], new=[],
  deps=["全部實作卡"], why="量產文件",
  accept="三頁變更紀錄各一行；文件衛生 grep 0"),
dict(id="P4-5", phase="P4", stage=9, repo="main", model="opus", effort="medium",
  title="手冊與安裝文件：Agent 獨立作業一章、安裝需求、資安例外登記兩條、出貨基線待重產回報",
  ren="extend：手冊既有章節結構；例外登記進 `docs/security-report/requirements/`",
  edit=["docs/user-manual/", "docs/security-report/requirements/"], new=["手冊章節"],
  deps=["全部實作卡"], why="手冊要把噪音表、離職流程、CIDR 逃生門寫到使用者看得懂",
  accept="章節齊；SR-08.2／SR-04.9 例外登記可查；文件衛生 grep 0；回寫明講出貨基線待重產。Agent repo `deploy/README.md` 的安裝需求段由 P1-6 順手改，本卡只引用"),
]

PHASE_NAME = {"P0":"底層","P1":"登入","P2":"離線能做事","P3":"接回主系統","P4":"LDAP 與收尾"}
REPO_NAME = {"main":"BE","fe":"FE 主站／console","jedi-packages":"jedi 套件","agent":"evidence-agent","test":"test"}

if __name__ == "__main__":
    import collections
    by = collections.defaultdict(list)
    for c in CARDS: by[c["stage"]].append(c)
    ids = {c["id"] for c in CARDS}
    for c in CARDS:
        for d in c["deps"]:
            if d.startswith("全部"): continue
            assert d in ids, (c["id"], d)
            dep = next(x for x in CARDS if x["id"]==d)
            assert dep["stage"] < c["stage"], f"{c['id']} (stage {c['stage']}) depends on {d} (stage {dep['stage']})"
    # 同批撞檔檢查（同 repo 內同一個既有檔）
    for st, cs in sorted(by.items()):
        seen = {}
        for c in cs:
            for f in c["edit"]:
                key = (c["repo"], f.split(" ")[0].split("（")[0])
                if key in seen: print(f"⚠ 批 {st}: {c['id']} 與 {seen[key]} 都改 {key}")
                seen[key] = c["id"]
    print("cards", len(CARDS), "stages", len(by))
    for st, cs in sorted(by.items()): print(f"批 {st}: " + ", ".join(f"{c['id']}({c['repo']})" for c in cs))
