"""FR-182 P3 拆卡底表（主系統端）。四張：兩張現在派（Stage 5）、一張等 P2（Stage 6）、一張等 FR-181 B-3／全部合回（Stage 7）。

與 FR-181（進行中，批 5 跑到 A-3）的撞檔核對（2026-11-07）：
- 181 套件工作區 `jedi-python-package/.worktrees/CM-3595` 動 jedi-agent-protocol `commands.py`／`__init__.py`、
  jedi-remote-agent `routing.py`／`agent_commands_route.py`／`command_channel.py`／`remote_agent_service.py`／
  `plugin/*`／`pyproject.toml`、jedi-detection 三支 client。**沒動** `events.py`、`infra/remote_agent/model/
  remote_agent.py`、`api/serializers/remote_agent.py`、`migrations/`、`app/service/heartbeat_*.py`。
  → P3-1 只改這幾個沒人動的檔，**不 bump 版號、不發版、不改 pyproject**；主系統開發期 path dependency
  指 181 合回後的套件主資料夾（`[worktree] link` 已含 `.venv`，path 裝一次工作區都吃得到）。
  發版＋釘版在 P3-4 收口卡（等 181 B-1 合回，它動主系統 `pyproject.toml`）。
- 181 B-3 動 FE `src/views/remote-agent/RemoteAgentManage.vue`＋主系統 i18n → P3-4 才加「授權」欄。
- 181 A-1 已合回 `core/plugins/remote_agent.py`（add_resource 掛點）→ P3-2 可在那裡掛一條；A-2 動
  `_agent_commands.py`／`_agent_config_notify.py`／`_agent_keys.py`／`agent_command_service.py`／`config/config.py`
  → P3 一個都不碰（P3-2 的 LC 位址用既有 `LICENSE_ACTIVATION_SERVER_URL`，不加 env）。
- 主系統 `core/plugins/_agent_status.py`、`_agent_events.py`：181 沒動 → P3-1 可改。

登記型檔 owner：jedi-agent-protocol `events.py`（P3-1）；jedi-remote-agent 模型／serializer／migrations（P3-1）；
主系統 `core/plugins/remote_agent.py` add_resource 段（P3-2）；`common/code/grc_error_code.py`（P3-2）；
主系統 `pyproject.toml`／`poetry.lock`（P3-4）；FE `RemoteAgentManage.vue`（P3-4）；Agent `license/__init__.py`／
FE `console/views/License.vue`（P3-3，接 P2 之後）。
"""

CARDS = [
dict(id="P3-1", stage=5, repo="jedi-packages", model="opus", effort="medium",
  title="契約與主系統落庫：agent_status 事件加 license 兩鍵、remote_agents 加兩欄、套件 migration 020（不發版、不 bump）",
  ren="extend：jedi-agent-protocol `events.py` `AGENT_STATUS_KEYS` 加 `license_status`、`license_expires_at`（**登記型，本卡 owner**；181 動的是 `commands.py` 不同檔）；jedi-remote-agent `infra/remote_agent/model/remote_agent.py` 加 `license_status: String(16) null`（`missing／invalid／mismatch／valid／notice／grace／expired`，NULL＝Agent 還沒回報過＝舊版 Agent）與 `license_expires_at: DateTime(tz) null`，`comment=` 寫清楚 NULL 語意；domain entity／query entity／repo impl 對應加欄（照 019 `health_flags` 怎麼加就怎麼加）；`api/serializers/remote_agent.py` 加兩欄；new：`app/service/heartbeat_license.py` `license_values_from_payload(payload, agent, now) -> dict`（照 `heartbeat_name.py` 形狀：值域白名單七態、`license_expires_at` 走 `_aware` 解析、格式不對一律不寫、有變才回）；`migrations/020-remote-agent-license.sql`（照 019：`ADD COLUMN IF NOT EXISTS`＋`COMMENT ON`，冪等）。主系統（**同一張卡順手改、但 repo 是 jedi-packages**——不行，一張卡一個 repo）→ 主系統那三行放 P3-2。🔴 **不 bump 兩個套件版號、不改 `pyproject.toml`、不發版**：181 同套件還在工作區，版號與發版在 P3-4 收口。測試：protocol `tests/test_events.py` 鍵在白名單；remote-agent `tests/test_heartbeat_license.py`（七態各一、壞值不寫、同值不回；**突變：拿掉值域白名單 → 塞 `hacked` 要紅**）。",
  edit=["jedi-agent-protocol/jedi_agent_protocol/events.py（AGENT_STATUS_KEYS 加兩鍵）", "jedi-remote-agent/jedi_remote_agent/infra/remote_agent/model/remote_agent.py", "jedi-remote-agent/jedi_remote_agent/domain/remote_agent/entity/remote_agent.py", "jedi-remote-agent/jedi_remote_agent/infra/remote_agent/repository/remote_agent_repo_impl.py（mapper 兩欄）", "jedi-remote-agent/jedi_remote_agent/api/serializers/remote_agent.py"],
  new=["jedi-remote-agent/jedi_remote_agent/app/service/heartbeat_license.py", "jedi-remote-agent/jedi_remote_agent/migrations/020-remote-agent-license.sql", "jedi-remote-agent/tests/test_heartbeat_license.py", "jedi-agent-protocol/tests/test_events_license_keys.py"],
  deps=[], why="照先例加欄位與白名單，規格明確；但 Agent 回報的值不可信，值域守門要有突變",
  accept="兩個套件各自 venv `pytest` 全綠（🔴 `env -u VIRTUAL_ENV`）；`grep -n license jedi-remote-agent/pyproject.toml jedi-agent-protocol/pyproject.toml` 為 0（版號沒動）；migration 020 在 DEV 主系統庫手套兩次（冪等）`\\d compliance.remote_agents` 看到兩欄；`AGENT_STATUS_KEYS` 含兩鍵；突變紅"),

dict(id="P3-2", stage=5, repo="main", model="opus", effort="medium",
  title="主系統：agent_status 落 license 兩欄、Agent 線上開通代打 LC（註冊 token 認證、主系統不落照）、開發期 path dependency",
  ren="extend：`core/plugins/_agent_status.py` `status_values` 加一行 `values.update(license_values_from_payload(known, agent, now))`（P3-1 的函式；本卡**同批平行**，先照契約名寫，181 合回前 import 失敗就 `try/except ImportError` 回 `{}` 並回寫）；`pyproject.toml` **開發期**改 jedi-agent-protocol／jedi-remote-agent 兩行為 path dependency（指 `~/Projects/Jedicogy/module/jedi-python-package/<套件>`，develop=true），`poetry update jedi-agent-protocol jedi-remote-agent`——**這兩行與 `poetry.lock` 不 commit**（`git update-index --assume-unchanged pyproject.toml poetry.lock`，回寫寫明；P3-4 還原釘版）；主線 migration **不寫**（套件 020 由主系統 installer 的套件 migration 機制套，照 017～019 先例；DEV 手套）。new：`api/remote_agent/routes/agent_license_route.py` `AgentLicenseActivateRoute.post`——守門：**沒有 `agent_pass_required`**（沒照的 Agent 還沒註冊沒通行證），改驗 body 的 `enroll_token`：`hash_token(token)` → 套件 `remote_agent_enroll_token_domain_service.get_enabled_by_hash(hash)`（**只驗不消耗**，register 才 `consume_enroll_token`；查無／停用／過期 → 401 `FILE_AGENT_TOKEN_INVALID` 沿用既有碼）；租戶只認 token 查出來的那個（照 `agent_enrollment_service.register` 兩段式，payload 帶什麼都不採信），LC 代打在該租戶機器身分下做＋`activation_code`＋`machine_fingerprint`（64 hex 格式驗）；body 上限 4 KB；IP 限流照 `/agents/register` 的（有就 reuse）；呼叫 new `app/remote_agent/service/agent_license_activation_service.py` `activate(code, fingerprint) -> dict`：`httpx.post(f\"{LICENSE_ACTIVATION_SERVER_URL}/api/activation/agent/activate\", json=…, timeout=10)`——錯誤對映**照 jedi-license-runtime `tenant_license_admin_service._call_lc`**：連不到→`GRC_AGENT_LICENSE_LC_UNREACHABLE`、400→把 LC 的 `error` 原句帶回（序號逾期／授權過期）、404→`GRC_AGENT_LICENSE_CODE_NOT_FOUND`、409→`GRC_AGENT_LICENSE_CODE_USED`、429→`GRC_AGENT_LICENSE_LOCKED`、401／503→`GRC_AGENT_LICENSE_LC_UNAVAILABLE`；成功回 `{\"license\": <LC 信封原樣>}`，**主系統不驗章、不落地、不記照內容**（Agent 自己驗；log 只記 `agent_fingerprint_digest`＋序號摘要＋結果）；`core/plugins/remote_agent.py` `mount_api=False` 那段 `api.add_resource(AgentLicenseActivateRoute, \"/agents/license/activate\")`（登記型，本卡 owner）；error code 五個進 `common/code/grc_error_code.py`（登記型，本卡 owner；FE `error-code.json` 同步兩語）；DI：service 進 `di_containers/remote_agent/`（181 A-1 動過已合回，可加）。`.env` 不加新變數（`LICENSE_ACTIVATION_SERVER_URL` 既有）。",
  edit=["core/plugins/_agent_status.py", "core/plugins/remote_agent.py（add_resource 一行）", "common/code/grc_error_code.py", "di_containers/remote_agent/remote_agent_containers.py", "pyproject.toml（開發期 path，不 commit）", "poetry.lock（不 commit）", "（FE）src/config/locales/i18n/zh-tw/error-code.json、en/error-code.json（五碼；FE repo 另 commit——一張卡一 repo，本卡回寫列出要加的五碼由首腦補）"],
  new=["api/remote_agent/routes/agent_license_route.py", "app/remote_agent/service/agent_license_activation_service.py", "test/test_agent_license_activation.py"],
  deps=[], why="守門形狀（註冊 token 而非通行證）、錯誤對映、不落照三件事要對；DI 簽章漂移是靜默的，驗收實打端點",
  accept="`pytest test/test_agent_license_activation.py`：token 無效 401；格式錯 400；mock LC 各 status → 對應 GRC 碼；成功回信封且 DB `tenant_licenses`／`remote_agents` 零寫入（**突變：把「不落地」拿掉寫一列 → 紅**）；`test/test_module_boundaries.py` 綠。DEV 實打：BE 起（path dependency 版）→ 產一把註冊 token → `curl -XPOST :8000/api/1.0/agents/license/activate -d '{enroll_token, activation_code:<LC DEV 簽的>, machine_fingerprint:<64hex>}'` 回 200 帶 `license`，拿去 LC `agent-verify` 過；同序號再打 409；錯 token 401 先於其他；`_agent_status` 收 `{license_status: valid, license_expires_at: …}` 事件後 `remote_agents` 兩欄有值、塞 `license_status: hacked` 不寫且 log warning"),

dict(id="P3-3", stage=6, repo="agent", model="opus", effort="medium",
  title="Agent 端接主系統：狀態事件送 license 兩鍵、主控台「線上開通」按鈕（序號→經主系統→LC→裝照）",
  ren="extend（接 P2 的檔）：`guidant_agent/license/__init__.py` `status_fields()` 回 `{\"license_status\": state.status, \"license_expires_at\": state.expires_at}`（P2-1 留的空 dict 填上；`agent_status` 元件「有變才送」靠既有 snapshot diff，不另加邏輯）；`guidant_agent/license/app/online_activate.py`（new）`activate_online(cfg, code) -> LicenseState`：`POST {cloud_url}/api/1.0/agents/license/activate` body `{enroll_token: cfg.registration_token, activation_code, machine_fingerprint: device_uuid}`（TLS 驗證走 `cloud_trust.cloud_tls_verify` 既有；逾時 15 秒）→ 回的 `license` 丟 P2-1 `install()`（同一條驗章＋落地）→ 回狀態；主系統各 GRC 碼對映成 `LicenseRejected(reason)`：`CODE_NOT_FOUND`／`CODE_USED`／`LOCKED`／`LC_UNREACHABLE`／`LC_UNAVAILABLE`／其餘 `remote_error`；`console/api/license_routes.py` 加 `POST /activate-online` `@require_session(\"configure\")` body `{activation_code}`（P2-2 的檔，P2 已合回才能動）。FE 畫面在 P3-3b（同批、FE repo）。🔴 `registration_token` 註冊完 FR-179 A3 會不再留在 `.env`——本卡先照現況讀 `cfg.registration_token`，A3 合回後要改成「主控台首次設定頁輸入的 token 暫存」，回寫標明。",
  edit=["guidant_agent/license/__init__.py", "guidant_agent/console/api/license_routes.py（加一支）"],
  new=["guidant_agent/license/app/online_activate.py", "test/test_license_online_activate.py"],
  deps=["P2-1", "P2-2"], why="接 P2 與 P3-2 兩邊契約，錯誤對映與 token 來源要對",
  accept="pytest：mock 主系統各 GRC 碼 → 對應 reason；成功 → `install` 被叫且狀態 valid；`status_fields()` 回兩鍵；守衛綠。本機 Agent（P2 合回版）對 DEV 主系統（P3-2 版）：主控台輸入 LC DEV 簽的序號 → 10 秒內「已授權」、sync 起來、主系統 `remote_agents.license_status=valid`"),

dict(id="P3-3b", stage=6, repo="fe", model="sonnet", effort="medium",
  title="主控台「授權」頁加「線上開通」：輸入序號一鍵開通（經主系統→LC），失敗原因六句",
  ren="extend（接 P2-3 的檔）：`console/views/License.vue` 尚未開通／已過期／mismatch 的卡下方加「線上開通」區——輸入序號＋「線上開通」按鈕（主系統連不上時仍顯示，點了回 `LC_UNREACHABLE` 那句）；成功 toast＋重拉 GET；失敗紅框對 i18n `reject.<reason>`；`console/licenseApi.js` 加 `activateOnline: (code) => request('POST', '/api/license/activate-online', { activation_code: code })`；`agent-console-license.json`（zh-tw、en）補 `online.title`／`online.code`／`online.button`／`online.hint`（「封閉網路請改走離線：把機器碼交給原廠」）與 `reject.<code_not_found|code_used|locked|lc_unreachable|lc_unavailable|remote_error>`。不自訂字級、禁 border-left、上下疊。",
  edit=["console/views/License.vue", "console/licenseApi.js", "src/config/locales/i18n/zh-tw/agent-console-license.json", "src/config/locales/i18n/en/agent-console-license.json"],
  new=[],
  deps=["P2-3"], why="加一區一支 API 六句文案，規格明確",
  accept="`npm run build:CONSOLE`、lint 無新 error；vitest：mock 各 reason 顯示對應句；`grep -c border-left console/views/License.vue` 為 0。手測（headless）等 P3-3 合回：輸入 LC DEV 序號 → 已授權"),

dict(id="P3-4", stage=8, repo="main", model="sonnet", effort="medium",
  title="收口：主系統釘版還原（吃 P3-4a 新版）、SPEC 與 Agent 手冊加「授權」、README 進度",
  ren="extend：主系統 `pyproject.toml` 還原釘版到 P3-4a 發的新版（jedi-agent-protocol 0.5.2、jedi-remote-agent 1.17.2）、`poetry update jedi-agent-protocol jedi-remote-agent`、`git update-index --no-assume-unchanged pyproject.toml poetry.lock`、commit；`docs/spec-site/current/` Agent 管理頁 spec 加「授權」一段＋變更紀錄一行（`writing-feature-specs` skill）；Agent 手冊「授權」一節（離線四步＋線上一鍵、封閉網路說明）；FR-182 README 總進度。**等 181 B-1 合回**（它也動 `pyproject.toml`）。",
  edit=["pyproject.toml", "poetry.lock", "docs/spec-site/current/<agent-manage 頁>.md", "docs/user-manual/agent/<主控台章>.md", "docs/features/FR-182-2611-agent-license/README.md"],
  new=[],
  deps=["P3-1", "P3-2", "P3-3", "P3-4a"], why="機械性：釘版與文件",
  accept="`pip show jedi-agent-protocol jedi-remote-agent` 是 Nexus 新版非 path；`git status pyproject.toml poetry.lock` 乾淨且 assume-unchanged 已取消；`pytest test/test_module_boundaries.py` 綠；spec build 綠；手冊 `grep -n \"原本\\|第 [0-9] 棒\"` 零命中"),

dict(id="P3-4a", stage=7, repo="jedi-packages", model="sonnet", effort="low",
  title="兩套件發版：jedi-agent-protocol 0.5.2、jedi-remote-agent 1.17.2（含 P3-1 的 license 鍵／欄／migration 020）",
  ren="extend：兩個套件 `pyproject.toml` bump（PEP 440 遞增；181 若已發過更高版就在其上 +1）、各自 venv 全部 tests 綠、`poetry publish --build -r nexus`（publish 不可逆，打錯 bump 下一號）；回寫版號與「主系統 P3-4 釘這版、Agent 不受影響（Agent 只用 protocol 的常數）」。**等 181 全部合回 `feature/FR-172`**（同套件同時有 181 的 commands 改動與本案的 events 改動，一起進版）。",
  edit=["jedi-agent-protocol/pyproject.toml", "jedi-remote-agent/pyproject.toml"],
  new=[],
  deps=["P3-1"], why="機械性發版",
  accept="Nexus 查得到兩版；`pip download jedi-agent-protocol==0.5.2 --no-deps` 成功；回寫版號"),

dict(id="P3-4b", stage=7, repo="fe", model="sonnet", effort="medium",
  title="主系統 Agent 管理頁加「授權」欄（七態 Tag＋到期日，舊版 Agent 顯示 —）、error-code 五碼",
  ren="extend：`src/views/remote-agent/RemoteAgentManage.vue` 列表在「連線」欄後加「授權」欄：`license_status` 對 Tag（valid 綠「正常」／notice 黃「N 天後到期」／grace 紅「寬限期」／expired 紅「已過期」／missing 灰「尚未開通」／invalid、mismatch 紅「照無效」；NULL → 「—」＋ tooltip「舊版 Agent 未回報」）＋ `license_expires_at` 日期；一格一值（Tag 一格、日期一格）；i18n `remote-agent.json`（zh-tw、en）；`error-code.json`（zh-tw、en、zh-cn 若有）補 `GRC_AGENT_LICENSE_*` 五碼。**等 181 B-3 合回**（同檔）。",
  edit=["src/views/remote-agent/RemoteAgentManage.vue", "src/config/locales/i18n/zh-tw/remote-agent.json", "src/config/locales/i18n/en/remote-agent.json", "src/config/locales/i18n/zh-tw/error-code.json", "src/config/locales/i18n/en/error-code.json"],
  new=[],
  deps=["P3-1"], why="加兩欄一組文案，規格明確",
  accept="`npm run build`、lint 無新 error；DEV 列表：123 Agent（P3-3 版）顯示「正常」＋到期日，未升版的 Agent 顯示「—」；`grep -c GRC_AGENT_LICENSE src/config/locales/i18n/*/error-code.json` 各 5；`grep -c border-left` 該檔不變"),
]


def check():
    ids = [c["id"] for c in CARDS]
    assert len(ids) == len(set(ids))
    stage_of = {c["id"]: c["stage"] for c in CARDS}
    for c in CARDS:
        for d in c["deps"]:
            if d in ids:
                assert stage_of[d] < c["stage"], (c["id"], d)
    seen = {}
    for c in CARDS:
        for f in c["edit"]:
            key = (c["stage"], c["repo"], f.split("（")[0].split("(")[0].strip())
            assert key not in seen, (key, seen.get(key), c["id"])
            seen[key] = c["id"]
    print("OK", len(CARDS), "cards;", {s: [c["id"] for c in CARDS if c["stage"] == s] for s in sorted(set(stage_of.values()))})


if __name__ == "__main__":
    check()
