---
name: project-minutes-sync
category: operations
project: project-minutes-sync
platform: claude
status: WIP
author: Peter Tu
sheetId: "3.21"
description: >-
  Close the 會議記錄 → 管控表 loop for one meeting: parse a [會議記錄] Doc's four tables
  (進度更新 · 提醒與阻礙 · 決議 · Action Items), then append-or-patch three 管控表 tabs —
  Action Items into 專案管理總表, 決議 影響 ≠ 無 into Change & Decision Log, Blocker 阻礙
  unresolved ≥ 7 days into Risk Register — and print a diff of what moved. Trigger on
  /project-minutes-sync or 「同步會議記錄」「把會議記錄同步到管控表」「會議決議同步」「Action Items
  進管控表」「阻礙開成 Risk」「sync the minutes」「把這場會的決議寫進 Change Log」. NOT
  project-note-specialist (3.08), which reshapes raw notes into the 4-section weekly update
  and touches no Sheet; NOT project-status-update (3.09), which READS the tabs this skill
  feeds and drafts the weekly mail; NOT project-init (3.20), which creates a project.
input: "A [會議記錄] Doc (URL or id) or a project slug that resolves via pm.json; the tracker Sheet id always comes from the adapter."
process: "知識來源 + pm.json check → parse the Doc's four tables per-table → detect the 管控表 version with pm-schema.py → Action Items append-or-patch tab 1 → 決議 → Change & Decision Log → 阻礙 → Risk Register → VALUES-ONLY write-back → diff report."
output: "Three 管控表 tabs updated by value only, the Doc's 已同步 checkboxes ticked on explicit go-ahead, and a chat diff report — appended · patched · skipped · unparsed · needs a human."
synergy: [project-status-update, project-note-specialist, project-init, admin-meeting-prep]
house-style: bound

---

# project-minutes-sync

```bash
npx skills add https://github.com/peter-tu-zynkr/zynkr-skill-builder --skill project-minutes-sync
```

會議記錄模板早就寫死兩條 MANDATORY 同步：一筆「決議」若影響範疇／時程／預算，必須同步到管控表的 Change & Decision Log；一筆「阻礙」若一週內無法自行解除，必須開一筆 Risk Register 並填 Owner 與狀態。這兩件事今天全靠手動，於是週報的 RECENT DECISIONS 讀的是一張沒人填的表。這個技能就是把那一段補起來——讀一份 Doc、寫三個分頁、回報一份 diff。它刻意很薄：不判定影響範疇、不代簽核准、不猜機率與衝擊、不寄任何信；判斷留給人，這裡只負責把已經寫下來的東西搬到它該在的表上，並把搬不動的部分點名。

---

## 這個技能與鄰居的分工

- **project-note-specialist（3.08）** — 把凌亂的筆記／逐字稿整理成四段週報格式；不碰任何 Sheet。它處理「文字長什麼樣」，本技能處理「這些文字該進哪張表」
- **project-status-update（3.09）** — 讀管控表出週報草稿。它的 RECENT DECISIONS 從 Change & Decision Log 讀，本技能就是餵那張表的人；週報相關的觸發詞一律歸它
- **project-init（3.20）** — 開案：複製模板、建資料夾、建管控表。本技能只在案子已經存在、也已經開過會之後才跑
- **admin-meeting-prep（3.03）** — 會前包：排程掃行事曆與信箱，會議前把行動提醒與 briefing 推到 Google Chat。本技能是同一場會的會後那一段
- **admin-meeting-note（3.04）** — `admin-meeting-prep` 底下的 agent（`agents/admin-meeting-note.md`），把會後的原始筆記／逐字稿整理成四段更新。它整理文字、不碰任何 Sheet，也不解析 `[會議記錄]` 的四張表；和 3.03 是兩個不同的東西，別把 id 對調

## 固定事實（先讀，別在這裡重述規則）

- `./references/pm-knowledge-pack.md` — §2 五個軸與三條裁決 · §3 管控表 v2 的 14 欄與強制版本偵測 · §4 編號與日期 · §5 Gate 與 Drop 留痕 · §6 升級與同步的兩條強制規則 · §9 讀這份知識包的技能四條鐵律
- `./references/pm-sources.md` — 8 份 PMO 模板 ID（§1）· `~/.config/zynkr/pm.json` adapter 契約與 Step 0 驗證順序（§2）· instance ID 只住 pm.json 這條規則（§2.3）
- `./references/pm-sheet-schema.json` — 分頁 → 標頭字串；本技能會寫的兩張表在這裡：`Risk Register`（10 欄）· `Change & Decision Log`（9 欄）
- `./references/pm-status-crosswalk.json` — 五個軸、各表面的值對照、`rejected_values`（`取消`／`暫停`）、`% = done / (total − dropped)`
- `./scripts/pm-schema.py` — 標頭與值的驗證 CLI（python3 標準庫，`--help` 可看退場碼）

**這五份都是技能自帶的，路徑一律以技能資料夾為根。** 安裝指令 只裝這一個資料夾；repo 的 `docs/pm-shared/` 與根目錄 `scripts/` 在安裝後的環境裡並不存在，指過去只會拿到 `No such file`。`scripts/check-pm-refs.sh`（住在 repo，給維護者跑）保證這五份與 `docs/pm-shared/` 逐位元組相同。呼叫 CLI 時把 seed 明給——`--schema ./references/pm-sheet-schema.json` · `--crosswalk ./references/pm-status-crosswalk.json`——因為 CLI 的內建預設值指向 repo 版面。

知識來源：references/pm-knowledge-pack.md · v1 · sha256 15640433fbee

Google 帳號、時區、週起始日與門檻值全部從 `pm.json` 讀（`pm-sources.md` §2）。本檔沒有、也不得出現任何專案的 Sheet／Doc／資料夾 ID。

---

## Step 0 — 知識來源與設定檢查

1. **知識來源**：算出手上這份知識包的 sha256 前 12 碼，和上面那行宣告比對。

   ```bash
   shasum -a 256 ./references/pm-knowledge-pack.md | cut -c1-12
   ```

   不一致就**停**，回報兩個值，並指向 `scripts/check-pm-refs.sh --sync`。宣告過期代表這份技能是照舊規則寫的，照跑等於用舊規則寫別人的表——停下來永遠比較便宜。

2. **設定**：讀 `~/.config/zynkr/pm.json`（可用環境變數 `ZYNKR_PM_CONFIG` 覆寫），依 `pm-sources.md` §2 的順序驗證：檔案存在 → 指定的 slug 在 `projects` 底下 → 它的 `engagement_type` 是 `engagement_types` 的 key → 這次要用到的 `tracker_sheet_id` 與 `minutes_doc_id` 非空、也不是 `<...>` 佔位。任何一關失敗就印出鍵路徑並停，例如 `config: projects.<slug>.tracker_sheet_id unset`。

3. **不猜 ID**：不得回頭用本檔的字面值、不得用 Drive 標題搜出「看起來對的那一份」、不得沿用上一次跑到的 ID。寫錯一張表，等於把一場會的決議寫進別人的專案。

## Step 1 — 輸入：一場會議、一個專案

- 接受兩種輸入：**(a)** 一份 `[會議記錄]` Doc 的 URL 或 ID；**(b)** 一個專案 slug，經 `projects.<slug>.minutes_doc_id` 解析成同一份 Doc。
- **管控表一律從 adapter 解析**：`projects.<slug>.tracker_sheet_id`。只給 Doc、沒給 slug 時，請使用者補上 slug——不要從 Doc 內文的連結反查 Sheet，那條路徑無法驗證。
- 一次只處理一場會議。多場會議＝多次執行，避免同批次的編號與去重互相污染。
- 記下這場會議的日期（Doc 標頭），後面 Change & Decision Log 的 `日期` 與 Risk Register 的 `最後檢視日` 都用它。

## Step 2 — 讀 Doc，逐表解析

用 `get_doc_content` 或 `get_doc_as_markdown` 取內容，必要時以 `inspect_doc_structure` 對照表格結構。若頂層 body 讀出來是空的，改帶 `tab_id` 再讀一次（Docs tabs 的已知行為），不要當成「這份 Doc 是空的」。

要解析的四張表，其餘（討論摘要 · 下次會議 · 出席）一律不動：

| 表 | 讀出來要用在哪 |
|---|---|
| 進度更新（`狀態 On track／At risk／Delayed／Done`） | Step 7，只讀不寫 |
| 提醒與阻礙（`類型 Call-out／Blocker`、`已同步 Risk Register？`） | Step 6 |
| 決議（`影響 範疇/時程/預算/無`、`已同步 Change & Decision Log？`） | Step 5 |
| Action Items（`Owner｜Due｜對應管控表任務 no.｜狀態`） | Step 4 |

規則：

- **以標頭字串定位欄位**，不以欄位順序；標頭找不到就當這張表解析失敗。
- **逐表隔離**：一張表解析失敗，另外三張照跑。報告要點名是哪一張、失敗在第幾列、什麼問題。
- **缺表要點名，不得當成空表**：找不到的表在報告寫「未找到（不視為空）」。「這場會沒有決議」和「決議表沒讀到」是兩件完全不同的事，混在一起會讓漏同步變成靜默成功。
- **日期缺年份不補**：知識包 §4 要求日期含年份；沒有年份的日期標 `（日期待確認）`，排除在所有日期運算外，列進報告。

## Step 3 — 偵測管控表版本（對映任何欄位之前）

1. 先讀表頭區塊而不是只讀第 1 列：`read_sheet_values(<tracker_sheet_id>, "專案管理總表!A1:N6")`。實際的表上，第 1 列常是核心目標、標頭落在第 3 列。**以第一個儲存格等於 `no.` 的那一列為標頭列**，並記住它的列號——資料列從它下面開始。找不到這樣的一列就停。

2. 把那一列丟給 CLI：

   ```bash
   python3 ./scripts/pm-schema.py headers \
     --tab 專案管理總表 \
     --headers "no.,里程碑 Stage,…" \
     --schema ./references/pm-sheet-schema.json
   ```

   | 退場碼 | 意思 | 這次執行怎麼辦 |
   |---|---|---|
   | 0 | v2，14 欄 A–N | 用 v2 對映往下跑 |
   | 2 | legacy v1，13 欄 A–M（沒有 `前置任務 Depends on`） | **回報後照跑**，改用 v1 對映（`K`＝`Reference 連結`·`L`＝`Note`·`M`＝`DOD`） |
   | 1 | 標頭不吻合任何一版 | **停**。印出 CLI 給的逐欄差異，不猜、不硬寫 |
   | 3 | 跑不起來（找不到 seed、參數錯——最常見的是漏了 `--schema`） | 這不是判定，是安裝／參數問題：回報並停 |

3. **不要用人工比對代替 CLI。** CLI 與它要讀的兩份 seed 都隨技能一起安裝（`./scripts/pm-schema.py` · `./references/*.json`），所以「拿不到 CLI」不再是一種正常情況——那代表安裝壞了或參數給錯，是要修的事，不是要繞過的事。退場碼 3 一律回報並停。

4. **永遠不得寫死範圍**。`project-status-update` 曾寫死 `A1:M44`，在 v2 表上把 `前置任務` 當成 `Reference` 讀——那是這批變更要修掉的活 bug，不要在這裡複製一次。

## Step 4 — Action Items → tab 1 專案管理總表

先把 tab 1 讀進來（標頭列以下全部），建立 `no.` → 列號的索引，再逐筆處理 Action Items：

- **有 `對應管控表任務 no.`，且該 no. 存在** → **PATCH**：只寫 `Owner` · `End (YYYY/M/D)` · `Status` 三欄，其餘一字不動。`任務描述`、`DOD`、`Note`、`前置任務`、`Reference` 都不覆蓋——會議記錄寫的是這一週的變化，不是這一列的全文。三欄裡沒給的那幾欄維持原值，不要用空字串洗掉。
- **有 no.，但 tab 1 找不到** → **衝突**：不寫、不新增、不改號，列進報告的「跳過」並附上原值。指到不存在的 no. 可能是打錯，也可能是有人刪過列，兩種都得由人看。
- **沒有 no.** → **APPEND**：依 `里程碑 Stage` 對到階段，取該階段現有最大的 `X.Y` ＋1，`Status` 一律寫 `Not started`。同一批次內自行遞增，避免兩筆撞同一個號。判斷不出屬於哪個階段時不要硬塞，列進「需人決定」。
- **永遠不得發明 `no.`**，也不得為了讓順序好看而重排既有編號——重排會讓所有 `前置任務` 參照失效（知識包 §4）。
- Action Item 的 `狀態` 只允許 lifecycle 表面四值 `Not started / WIP / Done / Drop`。出現 `取消`、`暫停` 之類的值，依 crosswalk 的 `rejected_values` 回報建議值（`取消` → `Drop`／`放棄`；`暫停` 是專案層級的狀態，任務要寫成 `WIP` ＋ Note 或 `Drop` ＋ 一筆 Change & Decision Log），**不自行換算**。要驗一批值：

  ```bash
  python3 ./scripts/pm-schema.py values \
    --values "Done,WIP" --axis lifecycle_sheet \
    --crosswalk ./references/pm-status-crosswalk.json
  ```

- 一列被改成 `Drop`，同一次動作必須在 Change & Decision Log 留一筆（知識包 §5）；會議記錄裡沒有對應決議時，把它列進「需人決定」，不要無聲地 Drop。

## Step 5 — 決議 → Change & Decision Log

- **篩選**：只取 `影響` 不是 `無` 的列（影響範疇／時程／預算）。`影響` 欄空白代表還沒判定，列進「需人決定」——判定影響範疇是人的工作，不是這個技能的（知識包 §6）。
- **一列一筆**，九欄依 `pm-sheet-schema.json` 的順序：`日期` · `類型 變更/決策` · `內容` · `原因` · `影響 範疇/時程/預算/無` · `提出人` · `核准人` · `狀態 提案/核准/駁回` · `關聯任務 no.`。
- **`狀態` 一律寫 `提案`**。這是 decision 軸，不是 lifecycle 軸——這欄永遠不會出現 `WIP` 或 `Done`。核准是人在 Gate 上做的動作，技能不代簽（鐵律 4）。
- `核准人` 沒有就留空，不要填提出人充數。`關聯任務 no.` 只在會議記錄寫了、且該 no. 存在時才填。
- **去重**：同一 `日期` ＋ 同一 `內容` 已經在表上就跳過，列進報告的 skipped-duplicate。同一場會被跑第二次不應該長出第二筆。
- 這一步存在的理由寫在知識包 §6：週報的 RECENT DECISIONS 從這張表讀，不從會議記錄讀。

## Step 6 — 阻礙 → Risk Register

- **升級條件三個同時成立**：`類型` ＝ `Blocker` · 仍未解除 · 距首次出現已達門檻天數。門檻讀 `defaults.health_thresholds.blocker_stale_days`（預設 7），對應知識包 §6 的「一週內無法自行解除」與 §2.2 at_risk 用的是同一條線。
- **「首次出現」怎麼算**：以這筆阻礙在會議記錄中最早出現的那場會議日期為準。只讀得到一場會議時，就用該場日期計算，並在報告寫明依據是哪一天——不要用「感覺拖很久了」升級。
- 欄位：`風險描述`（用原文，不改寫）· `類別`（會議記錄寫了才填）· `Owner`（**必填**）· `狀態 Open/Monitoring/Closed` ＝ `Open`（**必填**）· `最後檢視日` ＝ 這場會議日期。
- **`機率 1-3` · `衝擊 1-3` · `分數` 一律留白給人**。分數是機率×衝擊，直接決定要不要升級；猜一個數字等於替人做了升級決策。
- `緩解措施` · `應變計畫` 只在會議記錄原文寫了才填，沒有就留白。
- **Owner 缺就不寫**：把該筆列進「需人決定」。Owner 與狀態是這條規則的必填項，開一筆沒有 Owner 的風險等於沒開。
- `Call-out` 類型不升級。未達門檻天數的 Blocker 也不升級，但要在報告列成「觀察中（第 N 天）」，下次會議它就到期了。

## Step 7 — 進度更新只讀，永遠不寫進 Status

這一步單獨成段，因為它就是五個軸那條裁決存在的原因，也是這個技能最容易犯的錯。

- 進度更新那欄的 `On track / At risk / Delayed` 屬 **health 軸**，是一次讀數；`Done` 是 crosswalk 明寫的跨軸例外（`axes.health.cross_axis`），解析成 lifecycle 的 `done`。
- **永遠不得把 health 讀數寫進 tab 1 的 `Status`**；反向也一樣——`Status` 沒有日期不足以單獨變成一顆 RAG 燈。
- 這一步只做兩件事：讀出來、印進報告，並和知識包 §2.2 的推導比對——🔴 `delayed` ＝ `End < today` 且 lifecycle ≠ `done`；🟡 `at_risk` ＝（`End ≤ today + N` 且 lifecycle ＝ `not_started`）或有一筆未解除、已超過門檻天數的阻礙；🟢 `on_track` ＝ 其餘。那個 `N` 讀 `defaults.health_thresholds.at_risk_within_days`（預設 3），和 Step 6 的 `blocker_stale_days` 一樣是 adapter data——三個門檻是同一組可調數字，**不要把 3 寫死在這裡**，寫死等於在同一份檔案裡一邊讀設定一邊無視設定。
- 人填的讀數和推導不一致時，**兩個都報，一個都不改**。不一致本身就是要給人看的訊號。

## Step 8 — 寫回：只寫值，Doc 打勾要先問

**順序**：tab 1（Step 4）→ Change & Decision Log（Step 5）→ Risk Register（Step 6）。每張表一次寫入，寫完再寫下一張。

- 用 `mcp__google-workspace__modify_sheet_values`，`value_input_option="RAW"`——`2026/9/2` 這種字串照原樣落地，不被試算表重新解讀成別的格式。
- **append** 要先讀出該分頁目前的實際列數，寫在最後一列的下一列；不要用估的行號，也不要靠「應該有空白列」。**patch** 只寫那三個儲存格所在的範圍。
- **VALUES ONLY**：不重新轉檔、不改格式、不動欄寬、不排序、不插入或刪除列。這是一張正在被人使用的表，任何結構性動作都會弄壞別人的檢視與公式。
- 不碰 `所有檔案` · `Comms Plan` · `Budget` · `Stakeholders & RACI` · `Prerequisite Checklist`——本技能只寫三張表。
- **Doc 的兩個打勾欄要先取得明確同意**：`已同步 Risk Register？` 與 `已同步 Change & Decision Log？` 在會議記錄 Doc 上。把「要打勾的是哪幾列」列出來問使用者，得到明確同意才用 `batch_update_doc` 只改那幾格的文字。沒有同意就停在這裡：三張表的寫入照樣算數，報告寫明「Doc 未打勾」。寫別人正在編輯的 Doc 從來不是預設動作。
- 全程不寄信、不通知、不發任何訊息。

## Step 9 — 回報 diff

固定形狀，印在對話裡：

```
會議記錄同步 · <slug> · 會議日期 <YYYY/M/D>
知識來源 v<N> · sha256 <12 碼> · 管控表 <v2｜legacy v1>（pm-schema.py 判定）

新增
  專案管理總表          <n> 列  ← Action Items
  Change & Decision Log <n> 列  ← 決議（影響 ≠ 無）
  Risk Register         <n> 列  ← 阻礙（Blocker，已達 <N> 天）
更新（只動 Owner／End／Status）
  <no.> <任務>  Owner <舊>→<新> · End <舊>→<新> · Status <舊>→<新>
跳過
  <項目> — <no. 不存在／重複／缺 Owner／值不在四值內>
解析失敗
  <表名> — <第幾列、什麼問題>／未找到（不視為空）
health 讀數（只讀不寫）
  <項目> 人填 <值> · 推導 <值> · <一致｜不一致>
需人決定
  - <影響欄空白的決議／對不到階段的 Action Item／缺 Owner 的阻礙／沒有決議紀錄的 Drop>
Doc 打勾  <已打勾 n 列｜未打勾（未取得確認）>
```

沒有任何東西被寄出。報告只印在對話裡，讓人決定下一步。

---

## 硬規則

1. **只回報，永不寄送**。這個技能不產生任何郵件、訊息或通知。
2. **讀不到就回報，永不猜**。缺表點名、缺年份標記、標頭不吻合就停、`前置任務` 指向不存在的 `no.` 一律回報。
3. **Sheet 回寫只寫值**。不轉檔、不改格式、不動欄寬、不重排。
4. **永不發明 `no.`**。新增列依知識包 §4 遞增；衝突回報，不自行解決。
5. **跨軸不轉換**。health 讀數不進 `Status`；decision 的 `狀態` 不寫 lifecycle 的字；risk 的 `狀態` 不寫 decision 的字。
6. **不代人做判斷**。不判定影響範疇、不代簽核准人、不猜機率與衝擊、不替人決定該不該 Drop。
7. **ID 只從 `pm.json`**。本檔不含任何專案 ID，執行時也不從 Drive 標題猜。
8. **一次一場會議、一個專案**。

## House style

Writing style is **not owned by this file**. The house voice lives in two Google Docs under
`[@] 寫作指南` (`12DBdFz3SK22ie9im_ThFMI7IBRXsTZsV`), read at runtime:

- 《[2.0] Zynkr 通用風格指南 House Voice》 `10bOIQwRm9Pxwgct4hlwCwK_B4Pipai1HqBPZKzyRHSE` —
  the universal core, plus the addendum for this surface
- 《[3.2] 禁用詞清單 Forbidden Words》 `1N5sHLP4qzmmhpCGsi6KElxi1z0MFe4QZ0Q_35T10Uyg`

Read both before producing client- or reader-facing text, and scan the draft against 《[3.2]》
before handing it over. If Drive is unreachable, say so in the output rather than proceeding
unchecked. Never re-implement either list inside this file.
