project-minutes-sync · git:20260918.2429d96 · 2026-09-18 · sha256 58ab81dd512d992b

project-minutes-sync git:20260918.2429d96A

Immutable. This exact content never changes and is served at /api/v1/blob/58ab81dd512d992b.

---
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.