# Daily Activity Log

> Scan work repos (git), calendar events, PG/engineering emails, and Teams chats (SSP + customer) to build a structured daily activity log in customer-engagements. Writes activity-log.md per project as the source of truth for CRM sync and engagement tracking. Triggers include: 'daily activity', 'activity log', 'log my work', 'what did I do', 'sync activity', 'daily sync', 'update activity log', or any request to capture the day's SE work into customer-engagements.

- Skill: `roie9876/daily-activity-log` (Agent Skill)
- Install (CLI): `npx skillmds@latest add roie9876/daily-activity-log`
- Raw SKILL.md: https://api.skillmd.com/api/skills/roie9876/daily-activity-log/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Productivity
- Author: roie9876 (https://skillmd.com/u/roie9876)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/roie9876/daily-activity-log

---


# /daily-activity-log — Daily Activity Log Builder

Scan multiple data sources — work repos (git history), M365 calendar,
PG/engineering emails, and Teams chats (both SSP and direct customer) — to build
a structured daily activity summary per customer project. Writes `activity-log.md` in each project's
customer-engagements folder, making customer-engagements the single source of
truth for downstream consumers like `/crm-activity-sync`.

## Platform Compatibility

This skill runs on **macOS, Linux, and Windows**. Detect the OS first and pick the matching shell syntax. See `_shared/PLATFORM.md` (skills repo root) for the full translation table. Quick reference:

| Action | macOS / Linux (bash) | Windows (PowerShell) |
|--------|----------------------|----------------------|
| List dir | `ls $HOME/customer-engagements/` | `Get-ChildItem $HOME/customer-engagements/` |
| Find node | `which node` | `Get-Command node` |
| Run git in repo | `cd $HOME/repo && git log ...` | `Set-Location $HOME/repo; git log ...` |
| Home dir | `~` or `$HOME` | `$HOME` |

Default to POSIX commands; use PowerShell on Windows native (not WSL/Git Bash).

## Core Principles

- **Evidence-based only.** Every logged activity must trace to a concrete artifact: a git commit, a calendar event, or a sent email. Never infer or fabricate work.
- **Aggregate per day.** All evidence for a single customer + project + date is merged into one daily entry. A day of design commits + a meeting + an engineering email → one entry with sections, not three separate entries.
- **Append, never overwrite.** New daily entries are prepended (most recent first) to `activity-log.md`. Existing entries are never modified or removed.
- **Idempotent.** If the log already contains an entry for the target date + project, skip it. Report: "Activity for {customer}/{project} on {date} already logged."
- **Preserve source language.** Commit messages, email subjects, and meeting titles stay in their original language.
- **Graceful degradation.** Each data source can fail independently. If git is available but calendar isn't, log what you have. Note missing sources.
- **Classify, don't just list.** Each daily entry gets a **Task Category** classification based on the evidence. The category must match one of the 20 CRM `msp_taskcategory` values. The skill auto-classifies using signal keywords, but the user can override to any category during interactive mode or via the `default_activity_type` in `crm-mapping.json`.
- **Relevance-only content.** Every piece of text-based evidence (Teams messages, email body, meeting notes) must pass a **semantic relevance check** before being logged. You (the LLM executing this skill) read each message and judge whether it is project-related or off-topic. This is NOT a keyword filter — it is your semantic understanding of context. Personal chat, jokes, politics, social conversation, weekend plans, food, sports, and any off-topic messages are silently excluded — even if they appear in a customer or SSP chat. Only project-related business and technical content is logged. See "Relevance Classification" section below.

### CRM Task Categories (all 20)

These are the valid `msp_taskcategory` values in MSX. The skill can assign any of them:

| Category | Value | Auto-classify signals |
|---|---|---|
| **Architecture Design Session** | `861980004` | "architecture", "design", "topology", "solution review", design commits |
| **PoC/Pilot** | `861980005` | "poc", "pilot", "testing", "lab", "hands-on", poc commits |
| **Workshop** | `861980001` | "workshop", "deep-dive", "training", "enablement", multi-hour events |
| **Customer Engagement** | `861980000` | General customer meetings, sync calls, status updates |
| **Briefing** | `861980008` | "briefing", "ideation", "brainstorm", "explore", "concept" |
| **Blocker Escalation** | `861980006` | Engineering escalation emails, PG coordination on blockers |
| **Demo** | `861980002` | "demo", "demonstration", "showcase", "show and tell" |
| **Internal** | `861980012` | Internal-only meetings with project keyword match, no customer |
| **ACE** | `606820000` | "ace", "azure consumption" |
| **Call Back Requested** | `861980010` | "callback", "call back" |
| **Consumption Plan** | `861980007` | "consumption", "commit", "macc" |
| **Cross Segment** | `606820001` | "cross segment", "cross-segment" |
| **Cross Workload** | `606820002` | "cross workload", "cross-workload" |
| **External (Co-creation of Value)** | `861980013` | "co-creation", "hackathon", "joint development" |
| **Negotiate Pricing** | `861980003` | "pricing", "negotiate", "commercial", "deal terms" |
| **New Partner Request** | `861980011` | "partner", "isv", "si request" |
| **Post Sales** | `606820003` | "post sales", "post-sales", "go-live support" |
| **RFP/RFI** | `861980009` | "rfp", "rfi", "request for proposal" |
| **Tech Support** | `606820004` | "support", "incident", "troubleshoot" |
| **Technical Close/Win Plan** | `606820005` | "close plan", "win plan", "technical close" |

The top 8 are the most common for SE daily work. The remaining 12 are available for
specialized activities. If the auto-classifier can't determine the category, it falls
back to the project's `default_activity_type` in `crm-mapping.json`.

- **Detect unknown customers.** When a calendar event has external attendees whose domain doesn't match any existing `crm-mapping.json` customer, flag it. In interactive mode: ask whether it's a core customer, temp engagement, or not customer work. In automated mode: log the activity anyway (customer-engagements folder is source of truth if it exists) and send a Teams alert asking for classification.

## Prerequisite Auto-Install

Before running, verify all dependencies are present. **Install anything missing automatically.**

### Required Sibling Skills

This skill requires sibling skills from the same repository
(`https://github.com/roie9876/clawpilot-skills`):

| Skill | Purpose | Required? |
|---|---|---|
| `/crm-activity-sync` | Downstream — reads `activity-log.md` and pushes to CRM. Also owns the shared `config.json` and setup flow | ✅ For config + CRM sync |
| `/customer-repo` | Scaffolds `customer-engagements/` folder structure per customer | ✅ For initial setup |
| `/msx-crm` | CRM query tools (`run-tool.mjs`) for milestone and opportunity lookups | ⚠️ Only if CRM sync is desired |

Check if each required skill is installed:

```bash
# macOS / Linux
for skill in crm-activity-sync customer-repo msx-crm; do
  [ -f "$HOME/.copilot/skills/$skill/SKILL.md" ] && echo "✅ $skill" || echo "❌ $skill MISSING"
done
```

```powershell
# Windows
foreach ($skill in @('crm-activity-sync','customer-repo','msx-crm')) {
    if (Test-Path "$HOME\.copilot\skills\$skill\SKILL.md") { "✅ $skill" } else { "❌ $skill MISSING" }
}
```

**If ANY required skill is missing**, install all skills from the repository:

1. Clone the repo (skip if already cloned):
   ```bash
   # macOS / Linux
   [ -d "$HOME/customer-skills/.git" ] || git clone https://github.com/roie9876/clawpilot-skills.git "$HOME/customer-skills"
   ```
   ```powershell
   # Windows
   if (-not (Test-Path "$HOME\customer-skills\.git")) {
       git clone https://github.com/roie9876/clawpilot-skills.git "$HOME\customer-skills"
   }
   ```

2. Run the installer (idempotent — safe to re-run):
   ```bash
   # macOS / Linux
   bash "$HOME/customer-skills/scripts/install.sh"
   ```
   ```powershell
   # Windows
   pwsh "$HOME\customer-skills\scripts\install.ps1"
   ```

3. Verify the required skills are now installed. If still missing, stop and report the error.

### Required Tools

| Tool | Check (POSIX) | Check (Windows) | Install (macOS) | Install (Windows) |
|------|---------------|-----------------|-----------------|-------------------|
| git | `git --version` | `Get-Command git` | Pre-installed | `winget install Git.Git` |
| Node.js | `node --version` | `Get-Command node` | `brew install node` | `winget install OpenJS.NodeJS` |

Node.js is only strictly required if CRM sync is desired. Install any missing tools before proceeding.

### CRM Tool Script (`run-tool.mjs`) — Optional

If CRM sync is desired, the helper script must exist at `$HOME/.copilot/skills/msx-crm/crm-tools/run-tool.mjs`.

```bash
# macOS / Linux
[ -f "$HOME/.copilot/skills/msx-crm/crm-tools/run-tool.mjs" ] && echo "✅ CRM tools found" || echo "⚠️ CRM tools not found (CRM sync will be unavailable)"
```

```powershell
# Windows
if (Test-Path "$HOME\.copilot\skills\msx-crm\crm-tools\run-tool.mjs") { "✅ CRM tools found" } else { "⚠️ CRM tools not found" }
```

If missing and CRM sync is desired, clone the SE Kanban Tracker repo:

```bash
# macOS / Linux
git clone https://github.com/roie9876/crm-tools.git "$HOME/.copilot/skills/msx-crm/crm-tools"
cd "$HOME/.copilot/skills/msx-crm/crm-tools"
```

```powershell
# Windows
git clone https://github.com/roie9876/crm-tools.git "$HOME\.copilot\skills\msx-crm\crm-tools"
Set-Location "$HOME\.copilot\skills\msx-crm\crm-tools"; npm install
```

### M365 Sign-In

Check `m_m365_status`. If not signed in → call `m_m365_sign_in`.

### Additional Checks

| Prerequisite | Required? | How to check |
|---|---|---|
| `customer-engagements/` folder | ✅ | `$HOME/customer-engagements/` exists with ≥1 customer |
| Config file | ✅ | `$HOME/.copilot/crm-activity-sync/config.json` — shared with crm-activity-sync |

**Note:** This skill shares its config with `/crm-activity-sync`. If setup hasn't
been run yet, it will trigger the setup flow (see crm-activity-sync Step 1).

## Commands

| Command | Purpose |
|---|---|
| `/daily-activity-log` | Log yesterday's activities (default) |
| `/daily-activity-log today` | Log today's activities (so far) |
| `/daily-activity-log <date>` | Log activities for a specific date (ISO: 2026-04-21) |
| `/daily-activity-log week` | Log the current work week (Sun–Thu) |
| `/daily-activity-log range <start> <end>` | Log a date range |
| `/daily-activity-log add-repo <customer>/<project> <path>` | Register a work repo for a project |
| `/daily-activity-log remove-repo <customer>/<project> <path>` | Unregister a work repo |
| `/daily-activity-log repos` | List all registered work repo mappings |
| `/daily-activity-log add-domain <customer> <domain>` | Register an email domain for a customer (e.g., `checkpoint checkpoint.com`) |
| `/daily-activity-log domains` | List all customer domain mappings |
| `/daily-activity-log discover-chats` | Scan recent Teams chats to auto-discover customer conversations |
| `/daily-activity-log customer-chats` | List all cached customer chat IDs |

## File Locations

| File | Purpose |
|---|---|
| `~/.copilot/crm-activity-sync/config.json` | Shared config (user profile, preferences, repo mappings) |
| `~/customer-engagements/{customer}/projects/{project}/activity-log.md` | Per-project daily activity log (output) |
| `~/customer-engagements/{customer}/crm-mapping.json` | Per-customer project-to-CRM mapping (shared with crm-activity-sync) |

## Config: Work Repo Registration

Work repos are stored in `crm-mapping.json` under each project:

```json
{
  "projects": {
    "sase": {
      "display_name": "SASE",
      "work_repos": [
        {
          "path": "/Users/robenhai/SASE",
          "label": "SASE Design & POC",
          "added": "2026-04-23"
        }
      ],
      "subject_keywords": ["sase", "checkpoint", "aks", "dpdk", "srv6"]
    }
  }
}
```

**Registration rules:**
- A repo is registered to exactly one project. If a repo serves multiple projects (rare), ask the user which project it belongs to.
- Repos are registered manually via `/daily-activity-log add-repo` or during setup.
- Not every project has a repo — some are meeting-only engagements.
- The path must be an absolute path to a directory containing `.git/`.
- Validate on registration: `[ -d "<path>/.git" ]`. If not a git repo → reject.

## Config: SSP Chat Mapping

SSP-to-customer mapping comes from `config.core_ssps[]` in
`~/.copilot/crm-activity-sync/config.json`. Each SSP entry lists which customers
they cover. The 1:1 chat ID is resolved at runtime via
`m365_create_chat_by_email(email)` and can optionally be cached in
`crm-mapping.json` for performance:

```json
{
  "ssp_chat_ids": {
    "kobishitrit@microsoft.com": "19:80cb6140-...@unq.gbl.spaces"
  },
  "projects": {
    "sase": {
      "group_chat_ids": [
        {
          "id": "19:5fda07cb...@thread.v2",
          "topic": "HSASE-AZURE-GlobalNetworkBrain",
          "added": "2026-04-23"
        }
      ]
    }
  }
}
```

**SSP chat scanning rules:**
- Only scan chats for SSPs in `config.core_ssps`. Never scan random chats.
- Cache resolved chat IDs in `crm-mapping.json` under `ssp_chat_ids` to avoid
  repeated lookups.
- Group chats are optional — register via `/daily-activity-log add-group-chat`.
- The `group_chat_ids` array is per-project in `crm-mapping.json`.

**New commands for SSP chat management:**

| Command | Purpose |
|---|---|
| `/daily-activity-log add-group-chat <customer>/<project> <chat-id> <topic>` | Register a group chat for scanning |
| `/daily-activity-log remove-group-chat <customer>/<project> <chat-id>` | Unregister a group chat |

## Config: Customer Domains & Chat Discovery

Customer email domains are stored in `crm-mapping.json` per customer under a
top-level `domains` array. This is the primary mechanism for matching Teams chats
to customers:

```json
{
  "domains": ["checkpoint.com"],
  "customer_chat_ids": [
    {
      "id": "19:abc123...@unq.gbl.spaces",
      "type": "oneOnOne",
      "topic": "John Smith (CheckPoint)",
      "members": ["john.smith@checkpoint.com"],
      "discovered": "2026-04-24"
    },
    {
      "id": "19:def456...@thread.v2",
      "type": "group",
      "topic": "SASE Architecture Review",
      "members": ["john.smith@checkpoint.com", "kobishitrit@microsoft.com"],
      "discovered": "2026-04-24"
    }
  ],
  "projects": { ... }
}
```

**Domain registration rules:**
- Each customer can have one or more domains (e.g., `["checkpoint.com", "checkpoint.co.il"]`).
- Domains are matched against the email part after `@` in Teams chat member addresses.
- Register via `/daily-activity-log add-domain <customer> <domain>` or auto-discover.
- If `domains` is empty or missing, the customer is skipped during chat discovery.

**Customer chat discovery flow** (`/daily-activity-log discover-chats`):

1. Collect all known `customer_domains → customer` mappings from all `crm-mapping.json` files.
2. Call `m365_list_chats(limit: 50, expand: "members")` to get recent chats with member info.
   **Important:** Use `m365_list_chats` with `expand: "members"`, NOT `m365_search_chats`.
   Search only matches topic names and will miss unnamed chats or chats where the
   topic doesn't contain the customer name. Member-based scanning catches everything.
3. For each chat, check if any member's email domain matches a known customer domain.
   This catches: 1:1 chats with customer contacts, named group chats, AND unnamed
   group chats — as long as the customer member's email domain is registered.
4. If match → cache the chat entry in that customer's `crm-mapping.json` under `customer_chat_ids[]`.
   For unnamed chats, generate a descriptive topic like "(unnamed) member1, member2".
5. Skip chats that are already cached (match by `id`).
6. Report: "Discovered N new customer chats: {customer} — {topic/member}."

**Pagination:** If the user has many chats, page through using `skipToken` from the
response. Scan at least 100 chats (2 pages of 50) to ensure good coverage.

**Auto-discovery during daily runs:**
- Run discovery at the start of EVERY daily run (not just when `customer_chat_ids`
  is empty). New customer chats may appear at any time.
- Discovery only runs once per skill invocation (not per date in range mode).
- Cache results immediately so subsequent dates in the same run benefit.
- Scan at least 50 recent chats per run. This is fast (single API call).

**Manual fallback:**
- If auto-discovery misses a chat (e.g., member uses a personal email, domain is
  unusual), the user can manually note the chat ID and register it. But unlike SSP
  chats and group chats, there is no separate manual-add command — just edit
  `crm-mapping.json` directly or let discovery handle it.

---

## Step 0: Determine Target Date(s)

**Default:** Yesterday, adjusted for Israeli work week.
- If today is Sunday → yesterday was Saturday (weekend) → use Thursday.
- Otherwise → use yesterday (including Friday and Saturday — work can happen
  on non-standard days; the skill logs whatever evidence it finds).

**Note:** The old logic skipped Friday and Saturday entirely. This was wrong —
SEs sometimes work on non-standard days (CritSits, deadlines, travel). The
skill should always check yesterday. If no evidence is found, it reports
"No activity detected" which is harmless. Missing real work is not.

**Explicit date:** The user can provide a specific date or range.

**Week mode:** Sunday through Thursday of the current week up to yesterday.

Set `target_dates[]` — an array of dates to process. Process each date
independently, in chronological order.

---

## Step 1: Load Config and Mappings

1. Read `~/.copilot/crm-activity-sync/config.json`.
   - If missing → prompt: "Run /crm-activity-sync setup first to configure your profile and customer mappings."
   - Stop.

2. For each customer in `~/customer-engagements/`:
   - Read `~/customer-engagements/{customer}/crm-mapping.json`.
   - If missing → this customer has no CRM mapping yet. Still process it (activity log is useful even without CRM), but warn: "No CRM mapping for {customer}. Activities will be logged but won't sync to CRM."

3. Build a lookup table:
   - `repo_path → { customer, project }` — maps each registered repo to its project.
   - `keyword → { customer, project }` — maps subject keywords to projects.
   - `customer_domains → { customer }` — maps email domains to customers (from `crm-mapping.json` `domains[]` field, falling back to `stakeholders.md`).
   - `ssp_email → { customer[], chat_id }` — maps SSPs to their customers and cached chat IDs.
   - `customer_chat_ids → { customer, chat_id, members[] }` — maps cached customer Teams chats to their customer (from `crm-mapping.json` `customer_chat_ids[]`).
   - `all_customer_names → [...]` — list of all known customer display names, slugs, and account names for content-based matching.
   - `all_project_keywords → { keyword: { customer, project } }` — flattened keyword map across all customers/projects for content-based matching.

---

## Step 2: Collect Evidence

For each target date, collect from all sources in parallel.

**Source types:**
- **2a.** Work Repo Git History (registered repos)
- **2b.** Calendar Events (M365)
- **2c.** Sent Emails (PG/Engineering + Customer)
- **2d.** SSP Teams Chat (registered SSP chats)
- **2e.** Customer Teams Chat (registered customer chats)
- **2f.** Relevance Classification (filter for all text sources)
- **2g.** Content-Based Chat Discovery (unmapped chats — LLM-driven) ⚠️ **MANDATORY — see gate below**

> **⚠️ Step 2g is NON-OPTIONAL.** Steps 2a–2e only scan pre-registered sources.
> Step 2g is the only mechanism that discovers NEW customer chats (ad-hoc meetings,
> EXT support cases, CritSit chats, 1:1s about customer work). Skipping it causes
> silent data loss — entire customer interactions will be missing from the daily log.
> A mandatory gate before Step 3 enforces this.

### 2a. Work Repo Git History

For each registered work repo:

```bash
cd <repo-path> && git --no-pager log \
  --format="%h %aI %s" \
  --since="<target-date>T00:00:00" \
  --until="<target-date+1>T00:00:00" \
  --author="$(git config user.email)" \
  2>/dev/null
```

**Important:** The `%aI` format gives ISO 8601 author date — needed for duration
calculation (time span from first to last commit).

**Important:** Filter by `--author` to only count the user's own commits, not
collaborators. Use `git config user.email` from the repo, or fall back to the
email in config.

**Extract from commits:**
- Total commit count.
- Commit message prefixes (everything before the first colon: `design:`, `poc:`, `feat:`, `fix:`, `docs:`, `test:`).
- Dominant prefix = most frequent → drives activity type classification.
- Summarize what changed: group by prefix, list the distinct topics.

**Prefix-to-task-category mapping:**

| Git prefix | CRM Task Category |
|---|---|
| `design`, `arch`, `proposal` | Architecture Design Session |
| `poc`, `feat`, `experiment`, `spike`, `test` | PoC/Pilot |
| `workshop`, `lab`, `enable` | Workshop |
| `demo`, `showcase` | Demo |
| `docs`, `readme`, `changelog` | Architecture Design Session (documentation supports design) |
| `fix`, `hotfix`, `patch` | PoC/Pilot (bug fixing during POC) |
| `escalation`, `blocker` | Blocker Escalation |
| `support`, `incident` | Tech Support |
| No prefix / unknown | Use project's `default_activity_type` from crm-mapping.json |

**If a repo has zero commits on the target date** → skip it, no entry for this source.

### 2b. Calendar Events

```
m365_list_events(startDate: "<target-date>T00:00:00Z", endDate: "<target-date>T23:59:59Z", limit: 50)
```

**Filter criteria:**
- ❌ Exclude all-day events.
- ❌ Exclude cancelled events.
- ❌ Exclude recurring 1:1s with manager (match `config.user.manager.email`).
- ❌ Exclude events with zero external attendees AND no project keyword match in subject.
- ✅ Include events where ≥1 attendee has an external (customer) email domain.
- ✅ Include events where the subject matches a project keyword, even if all-internal.

**For each qualifying event, extract:**
- Subject, start time, end time, duration.
- Attendee list (names, classify as internal/external/engineering).
- Whether external attendees are customer or Microsoft Engineering (PG).

**Map to customer/project:**
1. Match external attendee domains against `customer_domains` lookup.
2. Match subject against `keyword` lookup.
3. If both match the same project → confirmed.
4. If domain matches one customer but subject matches another → flag ambiguity, ask user.
5. If no match → skip (not a customer event).

**Classify meeting category:**
- Meeting + "workshop" / "deep-dive" / "hands-on" / "training" → **Workshop**
- Meeting + "architecture" / "design" / "topology" / "solution review" → **Architecture Design Session**
- Meeting + "poc" / "pilot" / "testing" / "lab" → **PoC/Pilot**
- Meeting + "demo" / "showcase" / "demonstration" → **Demo**
- Meeting + "ideation" / "brainstorm" / "explore" / "concept" → **Briefing**
- Meeting + "rfp" / "rfi" / "request for proposal" → **RFP/RFI**
- Meeting + "pricing" / "negotiate" / "commercial" / "deal" → **Negotiate Pricing**
- Meeting + "close plan" / "win plan" / "technical close" → **Technical Close/Win Plan**
- Meeting + "partner" / "isv" / "si" → **New Partner Request**
- Meeting + "consumption" / "commit" / "macc" → **Consumption Plan**
- Meeting + "support" / "incident" / "troubleshoot" → **Tech Support**
- Meeting + "post sales" / "go-live" → **Post Sales**
- Meeting with Microsoft PG/Engineering (no customer) + blocker context → **Blocker Escalation**
- Meeting with Microsoft PG/Engineering (no customer) + project keyword → **Architecture Design Session** or **PoC/Pilot** (internal engineering work)
- Internal-only meeting matching project keyword → **Internal**
- Customer meeting (generic, no keyword match) → **Customer Engagement**

### 2c. Sent Emails (PG/Engineering + Customer Architecture)

```
m365_list_emails(folder: "sent", startDate: "<target-date>", endDate: "<target-date+1>", limit: 50)
```

**Include only emails matching these criteria:**

1. **Product Group / Engineering emails:**
   - Recipient domain is `@microsoft.com` AND
   - Recipient is NOT in your direct team (config.user known team members) AND
   - Subject matches a project keyword from any `crm-mapping.json`

2. **Customer technical emails:**
   - Recipient domain matches a known customer domain AND
   - This is a project-related thread (subject matches keyword)

**Exclude everything else.** No internal team emails, no SSP coordination, no HR/admin.

**For each qualifying email, extract:**
- Subject, recipients (names + emails), date.
- Classify: "Engineering Coordination" or "Customer Communication".
- Map to customer/project via subject keywords and recipient domains.

**If zero qualifying emails on the target date** → skip, no entry for this source.

### 2d. SSP Teams Chat (Ad-hoc Calls & Technical Discussion)

Ad-hoc Teams calls between SE and SSP are a major activity source that doesn't appear
on the calendar. These are captured by scanning 1:1 and relevant group chats with
known SSPs.

**For each SSP in `config.core_ssps`:**

1. **Find chat ID.** Use `m365_create_chat_by_email(email: "<ssp-email>")` to get the
   1:1 chat ID. Cache this — the chat ID is stable.

2. **Pull messages for target date:**
   ```
   m365_list_chat_messages(chatId: "<chat-id>", limit: 50)
   ```
   Filter to messages where `createdDateTime` falls within the target date (in user's
   timezone). Paginate with `skipToken` if needed to cover the full day.

3. **Detect ad-hoc calls.** Messages with `content: "<systemEventMessage/>"` and
   `type: "unknownFutureValue"` are Teams call events (call started/ended). Count
   pairs of consecutive systemEventMessages as one call. Each pair typically has
   timestamps ~seconds apart (start + end).

4. **Extract technical context.** From the non-system text messages on the same date,
   apply the **Relevance Classification** (see Step 2f). For each message:
   - First apply the quick scheduling filter: messages that are purely coordination
     like "זמין?", "פנוי?", "available?", "בפגישה", "שניה", "ok", "👍", single emoji,
     or messages shorter than 10 characters → classify as **Scheduling**, drop.
   - For all remaining messages, apply **semantic relevance check**: read the content
     and classify as **Relevant** (technical/business/project) or **Off-topic**
     (personal, jokes, politics, social). Only include **Relevant** messages.
   - Preserve original language (Hebrew, English, mixed).

5. **Map to customer/project:**
   - Use the SSP → customer mapping from `config.core_ssps[].customers[]`.
   - If the SSP covers multiple customers (rare), match by scanning message content
     against each project's `subject_keywords`.
   - If ambiguous → assign to the SSP's primary customer. Flag in the log.

6. **Summarize topics.** Group the technical messages into topics:
   - Extract the key nouns/concepts discussed (e.g., "SRv6", "NAT GW", "SSD",
     "ephemeral storage", "LB usage").
   - Note if any messages reference forwarded emails, documents, or PG contacts.
   - Count: number of ad-hoc calls, number of substantive messages.

**Also scan relevant group chats** registered in `crm-mapping.json`:
- If a project has a `group_chat_ids[]` array, scan those too.
- Apply the same date filter and technical content extraction.
- Group chats with SSP + PG engineers are especially valuable (e.g., HSASE-AZURE-*).

**If zero calls AND zero technical messages on the target date** → skip, no entry
for this source.

**Important filtering rules:**
- Only scan chats with SSPs mapped to active customers. Don't scan random chats.
- Messages from the SSP AND from the SE (you) both count as evidence.
- A day with calls but no text messages → still log as "ad-hoc call(s)" with
  no topic detail. The call itself is evidence of customer engagement.
- A day with technical text messages but no calls → still log. SSP text coordination
  is also customer work (e.g., answering SSP questions about the design async).

### 2e. Customer Teams Chat (Direct Customer Communication)

Direct Teams conversations with customer contacts — 1:1 chats and group chats
that include external customer members. Unlike SSP chats (2d) which are between
internal Microsoft people, these are conversations directly with the customer.

**For each customer that has `customer_chat_ids[]` in `crm-mapping.json`:**

1. **Iterate cached chats.** For each entry in `customer_chat_ids[]`:

2. **Pull messages for target date:**
   ```
   m365_list_chat_messages(chatId: "<chat-id>", limit: 50)
   ```
   Filter to messages where `createdDateTime` falls within the target date (in user's
   timezone). Paginate with `skipToken` if needed to cover the full day.

3. **Detect calls.** Same logic as SSP chats: messages with
   `content: "<systemEventMessage/>"` and `type: "unknownFutureValue"` are Teams
   call events. Count pairs as one call.

4. **Extract content.** From the non-system text messages on the same date,
   apply the **Relevance Classification** (see Step 2f). Same process as SSP chats:
   - Quick scheduling filter first (short/emoji/coordination → drop).
   - Then **semantic relevance check** on remaining messages: read each one and
     classify as **Relevant** (project work) or **Off-topic** (personal/social).
   - Only include **Relevant** messages in the log.
   - Preserve original language.

5. **Map to project:**
   - Check if the chat's `members[]` domains match a specific project's
     `subject_keywords` from the message content.
   - If the customer has only one project → assign directly.
   - If multiple projects exist → scan message content against each project's
     `subject_keywords` to find the best match.
   - If ambiguous → in interactive mode, ask the user. In automated mode, assign
     to the customer's default project (first in `crm-mapping.json`).

6. **Summarize.** Group messages into topics:
   - Extract key discussion points.
   - Note document shares, links, images.
   - Count: calls, substantive messages, and which members participated.

**If a customer has `domains` but no `customer_chat_ids`:**
- Run auto-discovery (see Config section above) before scanning.
- Cache results and proceed with scanning.

**If zero calls AND zero substantive messages on the target date** → skip, no
entry for this source.

**Important filtering rules:**
- Scan chats cached in `customer_chat_ids[]` here. Unmapped chats are handled
  by Step 2g (content-based discovery) — do NOT skip them entirely.
- Both your messages and customer messages count as evidence.
- A day with only a Teams call to the customer (no text) → still log as
  "customer call" — the call itself is engagement evidence.
- A day with only text messages (no call) → still log. Async customer
  communication is also work.
- If the same customer chat already contributed to a calendar event match
  (e.g., a scheduled Teams meeting that shows up in both calendar and chat),
  the calendar event takes precedence for that time block. But additional
  text messages outside the meeting window still count as separate evidence.
- Group chats with mixed internal + customer members: classify as customer
  chat (not SSP chat), since the customer is directly participating.

---

### 2g. Content-Based Chat Discovery (Unmapped Chats)

**This is the most important discovery mechanism.** Steps 2d and 2e only scan
chats that are pre-registered in `crm-mapping.json`. But customer work often
happens in chats that were never registered: ad-hoc support cases (EXT tickets),
1:1 with colleagues helping on customer issues, meeting chats from one-off calls,
new group chats created mid-engagement.

**The LLM (you) can read message content and understand which customer it relates
to.** This step uses semantic analysis — not domain matching — to discover
customer-relevant chats that other steps missed.

#### Discovery Flow

1. **Get recently active chats.**
   ```
   m365_list_chats(limit: 50, expand: ["members", "lastMessagePreview"])
   ```
   Filter to chats that had activity on any of the `target_dates[]`. Use
   `lastUpdatedDateTime` as the initial filter (it's fast — no message reads yet).

2. **Exclude already-scanned chats.** Remove chats that were already processed
   by steps 2d (SSP chats) or 2e (registered customer chats). These are identified
   by chat ID matching against `ssp_chat_ids` and `customer_chat_ids` in all
   `crm-mapping.json` files. Also exclude 1:1 chats with the user's manager
   (`config.user.manager.email`) — these are not customer work.

3. **Quick triage each remaining chat.** For each unscanned chat with target-date
   activity, read up to 20 messages from the target date:
   ```
   m365_list_chat_messages(chatId: "<id>", limit: 20)
   ```
   Filter messages to the target date (by `createdDateTime` in user's timezone).
   If zero messages on the target date → skip.

4. **LLM semantic classification.** For each chat with messages on the target
   date, you (the LLM) read the messages and answer these questions:

   a. **Is this customer-related work?** Look for:
      - Customer names, project names, product names matching known customers
      - Azure resource references (resource groups, subscriptions, VNets, PE)
        that match customer environments
      - Technical discussion tied to a known engagement (architecture, debugging,
        deployment, CritSit)
      - Support case context (EXT tickets, ICM incidents, CritSit)
      - Colleague names known to work on specific customer accounts (from
        `crm-mapping.json` SSP lists, stakeholders)
      - Chat topic keywords ("כלל ביטוח", "checkpoint", "sase", etc.)

   b. **Which customer and project?** Match against:
      - `all_customer_names` — customer display names, slugs, account names
      - `all_project_keywords` — subject keywords from all projects
      - Member names cross-referenced with known stakeholders/SSPs
      - Azure resource naming patterns (e.g., `rg-ai-clal-dev` → Clal Insurance)

   c. **What type of activity?** Classify using the same CRM Task Category
      signals as Step 3b.

   If the answer to (a) is NO → skip. Not customer work.
   If the answer to (a) is YES but (b) is ambiguous → in interactive mode, ask
   the user. In automated mode, log to the best-guess customer and flag
   "auto-classified — verify customer mapping."

5. **Process matched chats.** For each chat classified as customer-related:
   - Apply the same evidence extraction as Step 2e (calls, messages, topics,
     relevance classification from Step 2f)
   - Add the evidence to the appropriate `{ customer, project }` evidence bucket
   - **Auto-cache** the chat in that customer's `crm-mapping.json` under
     `customer_chat_ids[]` with `"source": "content-based"` so future runs
     find it via Step 2e directly (faster, no re-classification needed)

#### What This Catches That Domain-Based Discovery Misses

| Scenario | Why domain-based fails | Content-based catches it |
|---|---|---|
| EXT support case (e.g., "EXT \| 2604230050000351") | Members are MSFT + support vendor (LTI Mindtree, Teknowledge). No customer domain. | Messages reference customer name, Azure resources, and project context |
| 1:1 with internal colleague helping on customer issue | Both members are @microsoft.com | Messages contain customer resource groups, deployment errors, and project-specific terms |
| Meeting chat from ad-hoc customer call | Meeting wasn't in registered chats. Members may all be internal if customer joined via phone | Chat topic or messages reference customer by name |
| CritSit / ICM chat | Created by support, members are all internal + support vendor | ICM details, customer subscription IDs, resource names in messages |
| Cross-team collaboration | Engineers from other teams helping. No customer member present | Discussion clearly about customer architecture, deployment, blockers |

#### Performance Considerations

- **This step runs AFTER steps 2a-2e.** It only scans chats not already covered.
- **Quick exit:** Most chats are internal (team standup, org-wide, social). The
  LLM can classify these in seconds by reading the topic + first few messages.
  Only customer-related chats get full message extraction.
- **Caching eliminates re-work.** Once a chat is auto-cached in `crm-mapping.json`,
  future runs process it via Step 2e without needing LLM classification again.
- **Limit:** Scan at most 50 unmapped chats per run. This is enough for daily
  discovery. For backfill runs, page through more chats if needed.
- **Parallel with other steps:** This step can run in parallel with Steps 2a-2e
  since it targets different chats.

#### Auto-Cache Format

When a chat is auto-discovered via content-based classification, cache it in
`crm-mapping.json` under `customer_chat_ids[]`:

```json
{
  "id": "19:34791f8863eb455c860321ab3a702e08@thread.v2",
  "type": "group",
  "topic": "EXT | 2604230050000351",
  "members": ["Ido Katz", "Scott Vickers", "Jayant Kumar"],
  "discovered": "2026-04-25",
  "source": "content-based",
  "matched_by": "Messages reference Clal Insurance Azure resources (rg-ai-clal-dev-swe) and AI Foundry PE debugging"
}
```

The `source: "content-based"` and `matched_by` fields distinguish auto-discovered
chats from domain-matched ones. This helps the user audit and correct
misclassifications if needed.

---

## Step 2f: Relevance Classification (applies to ALL text sources)

Before any text-based evidence (Teams messages, email bodies, meeting chat) is
included in the activity log, apply **semantic relevance classification**. This is
not a keyword filter — it is an LLM judgment call made by you (the agent executing
this skill) when you read each piece of content.

### Classification Rules

For each message, email, or text excerpt, classify as one of:

| Classification | Action | Examples |
|---|---|---|
| **Relevant** | Include in activity log | Architecture discussion, requirements, design decisions, technical questions, pricing, timelines, deliverables, PG coordination, blocker discussion, status updates, action items, document sharing (technical), meeting scheduling about project topics |
| **Off-topic** | Silently exclude | Jokes, politics, sports, weekend plans, food, personal life, social chit-chat, memes, emoji-only reactions, holiday wishes, birthday messages, general small talk |
| **Scheduling** | Exclude (already filtered) | "זמין?", "available?", "ok", "👍", "שניה", "בפגישה", single emoji, <10 chars |

### How to Classify

Read the message content and ask: **"Does this message contribute to understanding
what work was done on the project?"**

- If YES → **Relevant**. Include it.
- If NO → **Off-topic**. Drop it silently. Do not mention it in the log.
- If MIXED (e.g., starts with a joke then pivots to a technical question) →
  **Relevant**. Include the technical part, summarize without the off-topic portion.

### Where This Applies

| Source | What gets classified | Notes |
|---|---|---|
| **SSP Teams chat (2d)** | Each non-system message | Replaces the old "scheduling chatter" blocklist with full semantic check |
| **Customer Teams chat (2e)** | Each non-system message | Same semantic check |
| **Sent emails (2c)** | Email subject + body preview | Already filtered by keyword + domain; relevance check is a second pass |
| **Calendar events (2b)** | Meeting subject | Already filtered by attendees + keywords; generally all relevant |
| **Git commits (2a)** | N/A — commits in registered repos are always relevant | No filter needed |
| **Group chats** | Each non-system message | Same semantic check |

### Counting Rules After Classification

- **Only relevant messages count** toward the `substantive messages` total in the
  Sources line (e.g., "customer-chat (1 call, 5 messages)" — those 5 are post-filter).
- **Calls always count** regardless of message content — a Teams call to a customer
  is work evidence even if the surrounding text is social.
- **Duration estimation** uses only relevant message count (5 min per relevant
  exchange, not per raw message).

### Edge Cases

| Case | Behavior |
|---|---|
| All messages in a chat on target date are off-topic | No entry from this source. The chat had no project-relevant activity. |
| Chat has a call + only off-topi

…(truncated)
