git:20260816.5c37a02 to git:20260907.364596e

35 added, 40 removed. Audit A to A.

---
name: consult-shadowing-scheduler
sheetId: "2.13"
description: >-
Close the shadowing-scheduling admin loop for a consulting deal in one pass: read
the CRM deal, resolve the client's existing [N] Drive project folder from the deal
notes, check Peter's calendar free/busy, propose 3–5 candidate slots, and on Peter's
confirm lay down the whole kit — a calendar hold, a "Shadowing — YYYY-MM-DD"
subfolder inside the project folder, a client logistics Gmail DRAFT (never sent),
a CRM follow-up task, and a deal-notes backlink. Trigger EAGERLY on
/consult-shadowing-scheduler or whenever Peter says "排 shadowing 時間", "約跟拍",
"安排現場觀察", "幫我跟 <client> 約 shadowing", "schedule the shadowing", "book the
shadowing session", "find time to shadow the client", or otherwise wants an on-site
observation session scheduled for a consulting engagement — even if he only names
the company. Distinct from consult-intake and consult-project-specialist (they
CREATE the project folder and the deal — this skill REUSES those artifacts and
never creates a new top-level folder or a new deal). Distinct from
consult-brd-writer (which consumes the shadowing transcript AFTER the session
happens — this skill only gets the session onto the calendar and preps the client).
category: sales-consultant
project: consult-shadowing-scheduler
platform: claude
status: Done
author: Peter Tu
input: "A CRM deal (URL or company name), optionally a preferred date window and session duration"
process: "get_deal → resolve the client's [N] Drive folder from deal notes → query_freebusy on Peter's calendar → slot-confirm gate → create the calendar hold + shadowing subfolder + logistics Gmail draft + CRM task → backlink + report"
output: "A confirmed calendar hold, a shadowing subfolder in the client's project folder, a client-ready logistics mail draft (never auto-sent), and a CRM task — all linked on the deal"
synergy:
- "consult-intake"
- "consult-project-specialist"
- "consult-brd-writer"
---
# Consult Shadowing Scheduler
```bash
npx skills add https://github.com/peter-tu-zynkr/zynkr-skill-builder --skill consult-shadowing-scheduler
```
After a consulting deal is qualified, the next step is a **shadowing session**
— Peter sits with the client's team and watches the real workflow run. Booking
it is pure admin friction. This skill collapses it into one pass: read the
deal, find the client's existing `[N]` project folder, propose free slots from
Peter's real calendar, then — on his pick — create the calendar hold, the
dated subfolder, the client logistics mail **as a Gmail draft (hard rule:
never sent)**, and the CRM task, backlinking everything onto the deal.
It is deliberately **gated, not autonomous**: nothing is created until Peter
confirms a slot — a wrong-day hold, or an invite accidentally mailed to the
client, is worse than one follow-up question. Re-runs update, never duplicate.
## How this differs from its neighbours
- **consult-intake / consult-project-specialist** — they CREATE the numbered
`[N]` folder and the deal; this skill assumes both exist and only adds a
subfolder + activity to them (no 專案資料夾 link → STOP and point at them).
- **consult-brd-writer** — consumes the shadowing transcript AFTER the session;
this skill runs before, and its subfolder is where that transcript lands.
## Fixed facts (don't re-derive these)
- - **Supabase project_id**: `uomieoqlkazknjgmfdda` (the shared Zynkr project; CRM tables are `crm_*`)
- **Google account** for all Gmail/Drive/Calendar tools: `peter_tu@zynkr.ai`
- **Drive parent folder** (`[2.2] 業務與顧問部門:專案`, home of the numbered `[N]` folders): `1hkXPX7OXPFOU0BcloPbJSFp8O0zArM8t` — orientation only; this skill never creates anything directly in it
- **CRM deal URL** for the report/backlink: `https://platform.zynkr.ai/deals/{deal_id}`
- - Owner (`Peter Tu`) is looked up live inside the SQL (`crm_users` by email) — never hardcode the id. `auth.uid()` is NULL over MCP, so every insert sets `created_by`/`assignee_id` explicitly.
+ - Ownership needs no lookup and no hardcoded id: every `mcp__zynkr__*` write is made as you, on your own workspace, and defaults the owner accordingly.
## Scheduling defaults (Peter overrides any of these by just saying so)
**Timezone** `Asia/Taipei` · **working hours** 09:30–17:30 · **slot-search window** next 10 business days · **session duration** 2 hours.
---
## Workflow
### 1 · Resolve the deal
- Peter hands over a deal URL or a company name. Preferred when the `zynkr` CRM
- MCP is connected: `mcp__zynkr__get_deal` (or `mcp__zynkr__list_deals` to find
- by company). Fallback via
- `mcp__supabase__execute_sql(project_id="uomieoqlkazknjgmfdda", query=...)`:
+ Peter hands over a deal URL or a company name.
- ```sql
- SELECT d.id, d.name, d.stage, d.notes, c.first_name, c.last_name,
- c.email AS contact_email, co.name AS company_name
- FROM crm_deals d
- LEFT JOIN crm_contacts c ON c.id = d.contact_id
- LEFT JOIN crm_companies co ON co.id = d.company_id
- WHERE d.id = '<deal uuid>' -- deal URL given
- OR d.name ILIKE '%<company>%'; -- name-only (confirm if >1 match)
- ```
+ - Deal URL → `mcp__zynkr__get_deal(id="<deal uuid>")`
+ - Company name → `mcp__zynkr__list_deals(search="<company>")`, then `get_deal`
+ on the match. More than one match → confirm with Peter before acting.
+ `get_deal` returns `notes`, `contact_id` and `company_id`. The contact name and
+ email come from `mcp__zynkr__get_contact(id=…)`, the company name from
+ `mcp__zynkr__get_company(id=…)` — the two follow-up reads that replace what the
+ old join returned in one row.
+
Capture the **contact name + email**, and scan `notes` for the 專案資料夾 line
that consult-intake / consult-project-specialist appended
(`專案資料夾:https://drive.google.com/drive/folders/<folder_id>`). Extract the
folder id — that `[N]` folder anchors everything this skill creates.
**No 專案資料夾 line → STOP.** Tell Peter the deal has no project folder yet
and point at `/consult-intake` (inbound lead) or `/consult-project-specialist`
(meeting debrief). Never invent a folder here.
### 2 · Collect constraints
Settle: date window · duration · on-site (現場) vs remote (遠端) — the last
one drives the mail's `{{MODE}}`/`{{LOCATION_OR_LINK}}`. Ask only for what's
missing; apply the defaults for the rest and **say which defaults applied**
(e.g. "用預設:台北時區、09:30–17:30、未來 10 個工作天、2 小時").
### 3 · Free/busy → 3–5 candidate slots
```
mcp__google-workspace__query_freebusy(
user_google_email = "peter_tu@zynkr.ai",
time_min = "<window start, RFC3339 +08:00>", time_max = "<window end>",
calendar_ids = ["peter_tu@zynkr.ai"]
)
```
Derive **3–5 candidate slots** that fit ALL of: inside working hours on
business days · full duration, zero overlap with busy blocks · **no same-day
adjacency to long meetings** (a day carrying a ≥2h meeting gets no slot butted
against it — shadowing is draining) · spread across days, not stacked on one.
### 4 · GATE — confirm the slot (nothing is created before this)
Present the candidates as a table and stop:
```
建議時段(Asia/Taipei · 2 小時):
| # | 日期 | 時間 | 備註 |
|---|------|------|------|
| 1 | 2026-08-12(週三) | 10:00–12:00 | 全天無其他長會議 |
| 2 | 2026-08-13(週四) | 14:00–16:00 | 上午空、下午僅此段 |
| 3 | 2026-08-17(週一) | 09:30–11:30 | 新週開場,前後乾淨 |
選一個,或直接給我你要的時段。
另外:要把客戶(王小明 <jane@example.com>)加進行事曆邀請嗎?
(加入 = Google 會「立刻」寄邀請信給對方)— 預設:否
```
Peter picks a number **or supplies his own slot** (his wins, even outside the
defaults). The **same gate** settles the attendee question: an external
attendee is emailed the moment the event is created, so the default is **NO**
— the client hears about the time via the reviewed draft. Explicit yes opts in.
### 5 · Create the kit (in this order)
**a) Calendar hold** — via `mcp__google-workspace__manage_event` (create):
```
mcp__google-workspace__manage_event(
user_google_email = "peter_tu@zynkr.ai", operation = "create",
summary = "[Shadowing] {{COMPANY}} — 現場跟拍",
start_time = "2026-08-12T10:00:00+08:00", end_time = "2026-08-12T12:00:00+08:00",
description = "Deal: https://platform.zynkr.ai/deals/{deal_id}\n專案資料夾: <folder url>",
attendees = [] # the client ONLY if opted in at the gate
)
```
Capture the event link. Remote sessions get a Google Meet conference.
**b) Shadowing subfolder** — inside the `[N]` folder, named
**`Shadowing — YYYY-MM-DD`**. Check for an existing one first (idempotency):
```
mcp__google-workspace__list_drive_items(user_google_email="peter_tu@zynkr.ai", folder_id="<[N] folder id>")
## no "Shadowing — 2026-08-12" folder yet →
mcp__google-workspace__create_drive_file(
user_google_email = "peter_tu@zynkr.ai",
file_name = "Shadowing — 2026-08-12", folder_id = "<[N] folder id>",
mime_type = "application/vnd.google-apps.folder", content = " "
)
```
This subfolder is where the session's **recording and transcript later land** —
consult-brd-writer's input home downstream. Note its URL.
**c) Logistics Gmail DRAFT** — read `./references/shadowing-mail-template.md`,
fill the placeholders from the deal + confirmed slot (zh-TW primary; EN
variant only if the deal correspondence is in English), then:
```
mcp__google-workspace__draft_gmail_message(
user_google_email = "peter_tu@zynkr.ai",
to = "<contact email from the deal>",
subject = "【Zynkr 顧問】{{COMPANY}} 現場跟拍(Shadowing)安排 — {{SESSION_DATE}}",
body = "<filled template>"
)
```
**Hard rule: this mail is a DRAFT, never a send.** Never call any send tool on
it — Peter reviews and sends it himself.
- **d) CRM task + notes backlink** — preferred: `mcp__zynkr__create_task` with
- the deal id, title **`跟拍 shadowing @ <YYYY-MM-DD>`**, due = the session
- date. SQL fallback (idempotent, consult-project-specialist's task pattern):
+ **d) CRM task + notes backlink** —
- ```sql
- WITH own AS (SELECT id FROM crm_users WHERE email = 'peter_tu@zynkr.ai' LIMIT 1),
- d AS (SELECT id, contact_id, company_id FROM crm_deals WHERE id = '{{DEAL_ID}}')
- INSERT INTO crm_activities (deal_id, contact_id, company_id, kind, subject,
- body, created_by, task_status, task_due_at, assignee_id)
- SELECT d.id, d.contact_id, d.company_id, 'task'::activity_kind,
- '跟拍 shadowing @ {{SESSION_DATE}}',
- E'行事曆:{{EVENT_URL}}\nShadowing 資料夾:{{SUBFOLDER_URL}}',
- (SELECT id FROM own), 'open'::task_status,
- '{{SESSION_DATE}}T09:00:00+08'::timestamptz, (SELECT id FROM own)
- FROM d
- WHERE NOT EXISTS (SELECT 1 FROM crm_activities a WHERE a.deal_id = d.id
- AND a.kind = 'task' AND a.subject = '跟拍 shadowing @ {{SESSION_DATE}}')
- RETURNING id;
- ```
+ **Check before you create.** The old statement was idempotent; `create_task` is
+ not, so a re-run would book the session twice. Call
+ `mcp__zynkr__list_tasks(filter="all", limit=200)` first and look for a task on
+ this deal whose subject is already `跟拍 shadowing @ <YYYY-MM-DD>`. Found →
+ skip, and say so. (That listing is capped at 200; if it returns exactly 200,
+ you cannot rule out a duplicate — ask Peter rather than guessing.)
- Then append the links to the deal notes (consult-intake's append pattern):
+ Otherwise `mcp__zynkr__create_task(deal_id="<deal_id>",
+ subject="跟拍 shadowing @ <YYYY-MM-DD>", body="行事曆:<event url>
+ Shadowing 資料夾:<subfolder url>", due_at="<session date>T09:00:00+08",
+ confirm=true)`. It is created as you and assigned to you — no ids to look up.
- ```sql
- UPDATE crm_deals SET notes = COALESCE(notes,'') ||
- E'\n\nShadowing {{SESSION_DATE}}:\n行事曆:{{EVENT_URL}}\n資料夾:{{SUBFOLDER_URL}}'
- WHERE id = '{{DEAL_ID}}';
+ Then append the links to the deal notes. `update_deal` REPLACES `notes`:
+ `mcp__zynkr__get_deal(id="<deal_id>")` first, append
+
```
+ Shadowing <YYYY-MM-DD>:
+ 行事曆:<event url>
+ 資料夾:<subfolder url>
+ ```
+ to what comes back, then `mcp__zynkr__update_deal(id="<deal_id>",
+ notes="<combined>", confirm=true)`.
+
Finally, **offer (ask-only)** a stage nudge: if the deal is still
- `new`/`contacted`, ask whether to move it to `qualified`
- (`mcp__zynkr__move_deal_stage`, or `UPDATE crm_deals SET stage`). Never nudge unasked.
+ `new`/`contacted`, ask whether to move it to `qualified`. On yes, use
+ `mcp__zynkr__move_deal_stage(id="<deal_id>", stage="qualified", confirm=true)`
+ — **only that tool.** Writing the column directly moves the stage but skips the
+ `stage_change` timeline entry and the automation event. Never nudge unasked.
### 6 · Report
```
Shadowing 已排定:範例科技 — 2026-08-12(週三)10:00–12:00
| 產出 | 狀態 / 連結 |
|------|------------|
| 時段 | 2026-08-12 10:00–12:00(Asia/Taipei · 2h)|
| 行事曆 | <event link>(未邀請客戶)|
| Shadowing 資料夾 | <subfolder url>(在 [N] 專案資料夾內)|
| 客戶信 | Gmail 草稿(待 Peter 審閱寄出)|
| CRM 任務 | 跟拍 shadowing @ 2026-08-12(due 當天)|
```
Name what still needs a human: sending the draft, and — if the client wasn't
invited at the gate — telling them the confirmed time.
---
## Idempotency — re-running the same deal + date
A re-run must **update, not duplicate** (consult-intake's rule): the
`list_drive_items` check in 5b reuses an existing `Shadowing — YYYY-MM-DD`
folder; the `NOT EXISTS` guard in 5d makes the task insert a no-op; for the
event, search the day (`mcp__google-workspace__get_events`) for a `[Shadowing]`
title on that company and `manage_event`-update it if the time changed, rather
than creating a second hold. A changed slot supersedes the draft — create the
new one, tell Peter to delete the old, append a correction line to deal notes.
## Why it's built this way
- **Gated, not autonomous** — a wrong artifact here has external blast radius
(a mis-set attendee emails the client instantly). One gate settles both
slot and attendee; before it, the skill only reads.
- **Draft-only client mail** — the mail carries commitments (date, recording
consent); Peter's voice check before send is non-negotiable.
- **Reuses the `[N]` folder** — numbering belongs to consult-intake /
consult-project-specialist; a second authority would fork the sequence.
- **The dated subfolder is the pipeline seam** — consult-brd-writer reads the
transcript from exactly `[N]/Shadowing — YYYY-MM-DD`.
- **Adjacency rule over raw free/busy** — a technically-free slot after a 3h
meeting is a bad shadowing slot; the judgment is encoded, not left to Peter.
## Reference files
- `./references/shadowing-mail-template.md` — the client logistics mail
(zh-TW primary + EN variant) with the placeholder table and filling notes.
## Limitations
- Requires a deal WITH a 專案資料夾 link in its notes — it stops (by design)
rather than create folders or deals itself.
- Slots come from **Peter's calendar only**; client availability is confirmed
via the mail draft, not negotiated live.
- Session output (recording upload, transcription, the BRD) is
consult-brd-writer's territory.
- The client is never emailed by this skill: the invite only if Peter opts in
at the gate, the logistics mail only when he sends the draft.