# Cron Job: morning-digest

**Job ID:** 5fd07b6b83f2
**Run Time:** 2026-07-24 05:01:36
**Schedule:** 0 5 * * *

## Prompt

[IMPORTANT: The user has invoked the "lfcs" skill, indicating they want you to follow its instructions. The full skill content is loaded below.]

---
name: lfcs
description: LF Construction Services site operations — job tracking, correspondence, inspections, RFIs, dockets, EOD summaries, and pre-pour checks. All skills share the same job folder structure under lfcs.jobs_root.
version: 1.0.0
category: lfcs
tags: [lfcs, construction, site-operations, job-tracking, admin]
metadata:
  hermes:
    requires_toolsets: [terminal]
    config:
      - key: lfcs.jobs_root
        description: Root directory holding active LFCS job folders. Must be the Jobs/ parent so both Ongoing Jobs/ and Upcoming/ are visible to the agent.
        default: "C:\\Users\\mclou\\rateright-growth-deploy\\HQ-Vault\\11_OpsMan_LFCS\\Operations\\Jobs"
        note: "Laptop path — use SSH to mclou@laptop-cd8df7fs to read. VPS vault at /home/ccuser/rateright-growth/HQ-Vault/11_OpsMan_LFCS/Operations/Jobs mirrors this but may lag."
      - key: lfcs.vps_jobs_root
        description: "VPS vault root for LFCS job folders. DISCOVERED PATHS (2026-06-29 — do not hardcode subfolder names, always ls first):"
        default: "/home/ccuser/rateright-growth/HQ-Vault/11_OpsMan_LFCS/Operations/Jobs"
        note: "Vault root = /home/ccuser/rateright-growth/HQ-Vault/11_OpsMan_LFCS/Operations/Jobs/ . Ongoing jobs subdir = 'Ongoing Jobs/' (WITH space). Hornsby job folder = '01-2275 - HPE Field of Play Hornsby' (does NOT match Airtable name 'Hornsby Field of Play'). Vault diary subfolder = '07 - Site Documents/' (NOT '07d - Daily Diary/')."
---

# LF Construction Services — Site Operations

All LFCS skills share the same job folder conventions. Every path below is relative to `<job>` = `<lfcs.jobs_root>/<Job>/`.

## Standard Job Folder Structure

```
<job>/
  00-brief.md                          ← contract price, type, start_date, target_completion
  _strategy.md                         ← profit-lever flags (optional)
  Job-Tracker.md                       ← hours table, materials table, costs-to-date
  Job Tracker - <job>.xlsx             ← alternative Excel format; check both if .md absent
  04 - Programme and Scheduling/
    programme-24wk.md                  ← 24-week programme, current week number
  06 - Correspondence/
    06a - Emails/                      ← drafted emails (EML-YYYY-MM-DD-description.md)
    06b - RFIs/_REGISTER.md            ← RFI register (RFI-A1, VO-001 numbering)
  05 - Safety and Compliance/
      05b - JSAs/_toolbox-talks.md      ← TBT running log (weekly entry pattern)
      05c - Inductions and Tickets/_inductions-checklist.md  ← crew tickets, HammerTech expiry dates
    06 - Correspondence/
      06a - Emails/                      ← drafted emails (EML-YYYY-MM-DD-description.md)
      06b - RFIs/_REGISTER.md            ← RFI register (RFI-A1, VO-001 numbering)
    07 - Site Documents/
      _inspection-log.md              ← inspection register
    07d - Daily Diary/                  ← directory containing `YYYY-MM-DD.md` entry files
  08 - Variations/_REGISTER.md         ← variation register (VO-001 upwards, status + client approval)
```

## Standard Job Fields (00-brief.md)

| Field | Values | Notes |
|-------|--------|-------|
| `type:` | `price`, `dayworks`, `hybrid` | Determines docket/photo requirements |
| `contract_price:` | dollars | Used for budget variance in job-tracker |
| `start_date:` | YYYY-MM-DD | Jobs in Upcoming/ with start_date within ±7 days are active |
| `target_completion:` | date | |

## Skill Map

| Skill | When to use | Key file |
|-------|-------------|---------|
| `lfcs-job-tracker` | "where's [job] at", "am I over budget" | Job-Tracker.md |
| `lfcs-eod-summary` | "EOD summary", "wrap up", 17:00 cron | Daily Diary + Job-Tracker |
| `lfcs-docket-check` | "docket check", EOD cron auto-check | 07a - Dockets/ |
| `lfcs-pre-pour-check` | "pre-pour check", "ready to pour" | 07c - Inspection Records/ |
| `lfcs-rfi-status` | "RFI status", "open RFIs", "what to chase" | 06b - RFIs/_REGISTER.md |
| `lfcs-client-correspondence` | Formal emails, letters, RFIs, variations | 06 - Correspondence/ |
| `lfcs-tool-tracker` | Tool/vehicle register, WhatsApp check-in/out | Google Sheets + Transfer Log |
| `timesheet` | "do my timesheet", weekly payroll submission | Job-Tracker.md hours table |
| `lfcs-weekly-snapshot` | Monday morning programme update, float ledger, RFI/VO/TBT/induction status | programme-24wk.md, _REGISTER.md files, _toolbox-talks.md, _inductions-checklist.md |
| `lfcs-am-lookahead` | AM morning briefing, 06:00 cron, today's plan + blockers + RFIs + deliveries | programme-24wk.md, RFI register, subbie schedule, Job-Tracker |
| `lfcs-morning-digest` | 05:00 cron — short Telegram digest (deadlines / top 5 unread / 7-day calendar / stale follow-ups). Gmail + Calendar only (LFCS email handled separately). Details: `references/morning-digest-google-fetch.md` | Gmail API, Calendar API, `google_token.json` |
| `site-diary` | **Rocky sends site update in Telegram** → parse → draft → approval → write Daily Record (Airtable) + vault diary | `~/.hermes/skills/site-diary/SKILL.md` |

**site-diary** — `~/.hermes/skills/site-diary/SKILL.md` (installed 2026-06-29). Skill Map entry: see `site-diary` skill. Arming protocol for new jobs:
1. Verify skill installed: `head -1 ~/.hermes/skills/site-diary/SKILL.md` → should return `name: site-diary`
2. PATCH Diary Channel ID to job record via Airtable API (PAT must have write scope — see PAT scope note in pitfalls)
3. Verify Daily Records table readable: GET `tbli76T3GvmLSd3Kd`
4. Confirm vault diary path: `<job>/07 - Site Documents/` (NOT `07d - Daily Diary/` — verify with ls, folder names vary by job)
5. Append to `_bridge/HANDSHAKE.md`: `site-diary armed for <job> — YYYY-MM-DD`
6. Reply "site-diary armed for [job]" in the channel

**Hornsby (2631) specifics — already armed (2026-06-29):**
- Diary channel: `7377499346`
- Airtable field: `fldxkoOIaFhWMYoQv` ("Diary Channel ID") on record `recZVyX89ijVz5v6k`
- Vault path: `/home/ccuser/opsman-work/Jobs/Ongoing Jobs/01-2275 - HPE Field of Play Hornsby/07 - Site Documents/`
- Note: vault folder name does NOT match Airtable job name — always ls to discover

## Active Job Identification

Check BOTH `<lfcs.jobs_root>/Ongoing Jobs/` AND `<lfcs.jobs_root>/Upcoming/`.
- A job in `Upcoming/` that has started site work is still active — do not skip it.
- Jobs with `start_date` within ±7 days of today are active.
- If there are multiple active jobs, handle each separately.

## Finding Full Job Data When Ongoing Jobs/ Is a Skeleton

**Failure mode:** A job shows in `Ongoing Jobs/` as active (site work started), but its folder there contains only skeleton files (`00-brief.md`, `Job-Tracker.md`, minimal subfolders). The rich data — full programme, RFI register, subbie schedule, variation register — still lives in the `Upcoming/` folder for that same job. This commonly happens when the job was set up for tender/pre-start but the move to "ongoing" was never completed by creating a full folder copy.

**Diagnosis checklist (run in order):**
1. `ls <job>/04 - Programme and Scheduling/` — if `programme-24wk.md` is empty (0 lines) or missing, the real data is elsewhere.
2. `ls <job>/06 - Correspondence/06b - RFIs/` — if `_REGISTER.md` has no rows or shows only a header, the real register is in `Upcoming/`.
3. `ls <job>/10 - Subcontractors/` — if the directory doesn't exist, the subbie schedule is in `Upcoming/`.

**Resolution:** When the Ongoing Jobs/ skeleton is empty, read the corresponding file from `<lfcs.jobs_root>/Upcoming/<job name>/`. Compare by checking `total_lines` — the non-empty file is the authoritative one. Log the discrepancy in today's diary entry: note which files had to be pulled from Upcoming/ vs Ongoing Jobs/.

**Why this matters:** The AM look-ahead and EOD summary will silently produce wrong outputs (empty programme, no RFI data) if they read the skeleton instead of the full folder. Always verify file content size before using a path — don't assume the path existence means the data is there.

## Handling Untracked Jobs

- RFI numbering: `RFI-A1`, `RFI-A3`, etc. (Cat A = mobilisation blockers, Cat B = scope, Cat C = commercial)
- Variation numbering: `VO-001` upwards
- Email drafts: `EML-YYYY-MM-DD-short-description.md`
- Diary entries: `YYYY-MM-DD.md`
- Docket files: `YYYY-MM-DD-signed.*` (strict pattern — no false positives)

## New Job Intake — Order of Operations (HARD RULE)

**Source of truth for job number ↔ client ↔ scope is Airtable, not Rocky's chat words.**

When Rocky sends a site photo + brief with a job reference (number, name, "cr plus", "that Peakhurst job", etc.), the correct sequence is:

1. **Airtable FIRST** — `mcp_airtable_search_records` (or PAT + curl) against the Jobs table BEFORE creating any folder. The MCP may be down; fall back to PAT. Do not skip this step just because the vault is empty.
2. **Match on the reference** — search by the digits / name Rocky gave. If you get a hit, that's the job. Use Airtable's `Name`, `Client`, `Job Number`, `Status`, `Bid Due` fields as the source for the brief.
3. **If Airtable has no record** — THEN fall through to the untracked-job protocol below.
4. **Only after source-of-truth resolution**, create the vault folder using the **Airtable name** (or a name Rocky confirms). Never invent a folder name from Rocky's casual phrasing — "cr plus" is not a name; "Clontarf Reserve Stage 1" is.

**Failure mode (2026-06-30, hit this):** Rocky said "This note is for 2649 cr plus job" + sent a pit site photo. The agent created `TBC - CR Plus - Construction Pit Site Visit/` without checking Airtable. Airtable held the real job: `2649 - Clontarf Reserve Stage 1 - North Shore Paving`. "CR Plus" wasn't a client name — it was a phonetic slip / mis-transcription, and the photos didn't visually match the actual scope. Folder had to be renamed and brief rewritten.

**Photo-vs-scope mismatch (2026-06-30, second failure):** Even after Airtable resolved the job, the 7 photos (silos, conveyor, drilling mud bay, drainage excavation against a building wall) did not match the Airtable scope (landscape reserve: paths, slabs, footings, timber platform). Don't silently attach the photos — flag the mismatch as an open question for Rocky in the brief. Possible causes: wrong photo attached, scope is broader than Airtable says, recent rain made the site unrecognizable.

## Handling Untracked Jobs (Job Referenced But Not in Vault or Airtable)

When Rocky mentions a job that doesn't appear in any search result:

**Failure mode:** Job exists in Rocky's mental model and on his local machine or Google Drive, but has no VPS vault folder and no Airtable record. Searching by location name, client name, or job number returns nothing.

**Immediate protocol:**
1. Acknowledge the gap — "I can't find [job name] in the vault or Airtable yet"
2. Ask Rocky for the **client name** and **job number** (even partial helps — e.g., "TCE job", "Liverpool job"). Airtable is the source of truth — if Rocky doesn't have a number, check Drive/laptop artefacts.
3. Check Rocky's **local machine** at `C:\Users\mclou\lfcs-inbound-artefacts\` via SSH (`ssh -i /root/.ssh/id_hermes_ed25519 mclou@laptop-cd8df7fs`)
4. If artefacts found locally, create the vault folder structure and 00-brief.md before proceeding — using the **client's name from the artefact**, not Rocky's casual phrasing
5. If no artefacts found locally, create a skeleton job folder with `00-brief.md` (type, client, notes) and flag for Rocky to supply artefacts
6. Never assume the job doesn't exist — it may simply not have been synced to the VPS vault yet

**Never say:** "That job doesn't exist." Say: "I don't have it in the vault or Airtable yet — what's the client name?"

**Job number mismatch — clarify before assuming it's a new job:**
- Rocky may say "LF 2651" but the job is actually "2626" — a one-digit slip
- Rocky may say "cr plus" but the client is "North Shore Paving" — a phonetic/memory slip (2026-06-30 incident)
- Before creating a new job folder, always: (a) check Airtable Jobs table on the number, (b) check the Ongoing Jobs directory listing, (c) if both empty, ask: "Is that [Rocky's words] or [Airtable candidate]?"
- This avoids a duplicate job folder being created for something that already exists
- If searching by job number returns nothing in Airtable, try: (a) partial substring search, (b) ask for the client name, (c) check laptop inbound-artefacts

## Role Scope — Foreman's Pocket PM

**Hermes is the foreman's pocket super: PM + estimator + admin + engineer in one.**

When Rocky is on site, I am his daily companion. The full scope:

- **Capture:** site notes, photos, hours, deliveries, docket status, variations, RFI triggers
- **Recall:** answer "what did I do Tuesday?", "what's the rate for X?", "is docket A signed?", "is this in scope?"
- **Remind:** docket-not-signed, RFI-overdue, pour-tomorrow, materials-due, 48-hour variation clock running
- **Route:** PM territory (formal letters, contract interpretation, programme analysis) → tell Rocky "CC will draft this from the desk"
- **Draft:** variations immediately when triggered (48-hour clause is non-negotiable), pre-pour checklists, RFI drafts, EOD summaries
- **Programme:** hold the baseline + progress schedule, tell Rocky how to get back on track when behind
- **Specs:** make the job specs, pre-pour checklists — Rocky prints on site or I file for him
- **Scope answers:** when a foreman asks "is this in my scope?", I answer from the drawings + scope of work — not guess

**NOT my job:** formal commercial letters, contract interpretation calls, strategic programme replanning, long-form reports. Capture the trigger, route to Claude Code on the laptop.

## Claude–Hermes Bridge — `_bridge/HANDSHAKE.md`

**Path:** `.../Operations/_bridge/HANDSHAKE.md`

**Purpose:** Status/notes file where Hermes and Claude Code leave each other messages when working simultaneously on the same job. Created by Hermes on first session start.

**Usage:**
- Hermes appends: `HERMES online — YYYY-MM-DD` on startup
- Claude Code appends: status when picking up work or handing off
- Never edit history — append only

## Coordination with Claude Code

- My daily diary must be thorough enough for CC to draft commercial letters from
- Variations I log need: date, trigger, description, dollar estimate, status
- CC reads `daily/{date}.md` + Variations register before drafting anything commercial
- CC handles: formal variation claims, EOT notices, payment claim disputes, programme analysis, contract clause readings

## Communication Mode — TEXT ONLY (HARD RULE)

**WhatsApp groups: READ-ONLY. Telegram is the live channel.**
- Rocky manages LFCS from site via **Telegram** (1:1 with Hermes)
- WhatsApp groups: Hermes receives site updates but **cannot reply** unless @mentioned, in a 1:1 DM, or Rocky/Hasibul/Liam directly ask
- Never reply to onboarding prompts, error messages, or status updates in WhatsApp groups
- All live foreman communications happen in Telegram

**Rocky has explicitly said: "stop giving me voice messages, just do in text."**
- NEVER send voice memos or TTS audio to Rocky via Telegram
- If Rocky asks for a voice explanation → deliver short bulleted text he can read on his phone
- If you accidentally send audio → switch to text immediately, do not repeat the mistake

**Why:** Audio requires download/play on his Telegram — he's on-site, one hand occupied, noisy environment. Text he skims in 5 seconds.

## Verification Before Acting — Ground Truth Over Config Labels

**Rocky's rule (set 2026-07-05):** When a question is about identity / version / state of a system, run the live verification command and report what it returns. **Do not quote config files, memory entries, or prior-session notes when a live probe is available.** If config and ground truth disagree, flag the conflict, fix the record, proceed.

This is a workflow discipline, not a single pitfall. Concrete applications already proven:

| Question | Don't quote | Run instead |
|---|---|---|
| Which Telegram bot am I? | `config.yaml`, SOUL.md, MEMORY.md `first_name`/`username` | `curl -s "https://api.telegram.org/bot$(grep ^TELEGRAM_BOT_TOKEN= /root/.hermes/.env \| cut -d= -f2-)/getMe"` — report `result.username`. **Always.** Config labels for bot identity are demonstrably stale (proven 2026-07-05 — SOUL.md said `@HermesLouBot`, `getMe` returned `@opsman_watchdogbot`, same token drives both DM agent and watchdog cron). |
| Which model is this session on? | MEMORY.md pin note | `hermes config show \| grep -E "default\|provider"` |
| Does this Airtable PAT have write scope? | `meta/bases` `permissionLevel` | Live `PATCH` against a known record, check 200 vs 403 |
| Is `hermes-gateway` running right now? | `ps aux \| grep hermes-gateway` (false positives — PID 2636331 in audit turned out to be a defunct shell, not a live restart) | `systemctl is-active hermes-gateway` + `systemctl status` + `ps -o etime` cross-check |
| Is the Telegram bot receiving a specific group's messages? | UI screenshots showing double blue ticks (those go to ALL members, not the bot's update queue) | `getUpdates` with `allowed_updates` covering `message,my_chat_member,chat_member` — empty result = the bot's update queue has nothing = the bot is not actually receiving from that group |
| Which Airtable record matches a job number? | Vault folder name | `search_records` (or PAT + curl) on Jobs table by `Job Number` |
| Does this Telegram group chat ID resolve via API? | Telegram invite-hash link text | `getChat` with the chat_id — note that **private-group invite hashes cannot be resolved by `getChat` unless the bot is already a member**, and `joinChat` with the hash returns `404 Not Found` for restricted / non-permanent links regardless of validity |

**Failure pattern:** quoting the stale label produces silent wrong actions. User has to chase the agent for "are you sure?". The fix is one curl call. **Always reach for the probe first when the question is "what is X really?"**

## Shared Pitfalls

1. **Airtable MCP unreachable — use PAT + curl fallback.** When `mcp_airtable_list_bases` returns `MCP server 'airtable' is not connected`, the MCP server process is down. Do NOT keep retrying mcp_airtable tools. Use direct REST API via `curl` with the PAT instead. See "Airtable — MCP Server + PAT" section above for commands and PAT value.
2. **WhatsApp groups are READ-ONLY.** Never reply in groups unless @mentioned, 1:1 DM, or Rocky/Hasibul/Liam directly ask.
2. **Job-Tracker.md tables sometimes use `$` and `,` — strip both before parsing numbers.
3. For `dayworks`/`hybrid` jobs: dockets required. For `price` jobs: no docket needed.
4. Use Australia/Sydney timezone for all date comparisons.
5. If a field can't be parsed, show raw cell with `(see file)` rather than guessing.
6. Never auto-send client correspondence without Hasibul review for external emails.
7. **VPS path for job folders:** Job folders live at `/home/ccuser/opsman-work/Jobs/`. Always `ls /home/ccuser/opsman-work/Jobs/` first — subfolder names contain spaces ("Ongoing Jobs") and must be quoted in all shell commands.
8. **Always `ls` a directory before reading files inside it.** On the VPS vault, `07d - Daily Diary/` and `07a - Dockets/` are **directories** (not files).
9. **Job folder names have spaces:** "Ongoing Jobs" and "01-2275 - HPE Field of Play Hornsby" both have spaces. Always quote paths in shell commands when they contain spaces.
10. **Job-Tracker.md is empty (zero rows):** This is a real finding — report "no entries logged today" explicitly. Do not skip the hours/materials section or imply a diary exists when it doesn't.
11. **Airtable PAT location on Hermes: NOT in shell env.** `$AIRTABLE_API_KEY` returns empty in
    cron and most terminal contexts. The real PAT lives at
    `~/.hermes/config.yaml` → `mcp_servers.<airtable>.env.AIRTABLE_API_KEY`. Load it with Python:
    `yaml.safe_load(open('/root/.hermes/config.yaml'))['mcp_servers']['airtable']['env']['AIRTABLE_API_KEY']`.
    **NEVER name which PAT is current — read it from config.yaml, every time.** This note used to
    say "the PAT currently wired in is `patIQTejsB…`" and "the broken PAT `patL6HMyw…` is stale, do
    not propagate". Both were WRONG, in the worst possible direction: `patIQTejsB…` was REVOKED, and
    the one called stale was **LIVE with `create` scope on the base**. The token actually in use
    (`patTsSy5G…`) was named nowhere. That inversion is why a working create-level credential sat for
    weeks in a file labelled broken — nobody hid it, a doc lied and everyone believed the doc.
    A note that names the current secret must be re-edited on every rotation, and it will not be.
    config.yaml is the source of truth; ask it, don't remember it.
    **NEVER paste a PAT into a reference file, an example, or a curl line — load it at runtime.**
    Reference files get read into model context, and this box's provider is `minimax` (PRC): a token
    written here is a token shipped offshore. That is not theoretical — the daily cron outputs
    CONTAINED the token string, because the model ingested it from these files and re-emitted it.
    `GET /v0/meta/bases` is also unreliable: it returns `permissionLevel: "create"` for both
    read-only and write-capable PATs. **Test PATCH to confirm write scope; don't trust meta.**
    Use direct REST API via `curl` or `urllib.request`, NOT the MCP server (intermittent).
14. **Airtable upsert helper — always wrap with `{"fields": ...}`.** POST accepts the inner dict
    directly, but PATCH requires `{"fields": {…}}` — sending the inner dict on PATCH returns
    `422 INVALID_REQUEST_MISSING_FIELDS` and silently breaks every subsequent update. If a helper
    script's write-probe passes with an empty-fields PATCH but real writes fail, this is the cause.
    Full post-mortem + the one-line fix at `references/airtable-upsert-bug-2026-07-10.md`.
12. **Drive private files need a cookie session or public access.** `curl -L` on a private Drive link sometimes returns HTML auth page instead of the file. Retry once — sometimes the second request follows the redirect correctly. If it persists, make the file public ("Anyone with the link" → Viewer) or re-auth Gmail to get an OAuth token.
13. **Laptop SSH — Windows cmd.exe session.** SSH to `mclou@laptop-cd8df7fs` opens a Windows `cmd.exe` session. Unix commands (`ls`, `find`, `cat`, `which`) all fail with "'X' is not recognized." Use PowerShell commands: `powershell -Command "Get-ChildItem ..."` or SSH with `-o PreferredAuthentications=publickey` and a Windows SFTP/SCP client instead.
**Vault diary path for 2631:** `.../01-2275 - HPE Field of Play Hornsby/07 - Site Documents/{YYYY-MM-DD}.md` (append-only, never edit history)

**Both must work:** The 48-hour variation notice rule is meaningless without email. Every LFCS function depends on having these tools.

## Shared Pitfalls

## Shared Pitfalls

1. **Airtable MCP unreachable — use PAT + curl fallback.** When `mcp_airtable_list_bases` returns `MCP server 'airtable' is not connected`, the MCP server process is down. Do NOT keep retrying mcp_airtable tools. Use direct REST API via `curl` with the PAT instead. See "Airtable — MCP Server + PAT" section above for commands and PAT value.
2. **WhatsApp groups are READ-ONLY.** Never reply in groups unless @mentioned, 1:1 DM, or Rocky/Hasibul/Liam directly ask.
2. **Job-Tracker.md tables sometimes use `$` and `,` — strip both before parsing numbers.
3. For `dayworks`/`hybrid` jobs: dockets required. For `price` jobs: no docket needed.
4. Use Australia/Sydney timezone for all date comparisons.
5. If a field can't be parsed, show raw cell with `(see file)` rather than guessing.
6. Never auto-send client correspondence without Hasibul review for external emails.
7. **VPS path for job folders:** Job folders live at `/home/ccuser/opsman-work/Jobs/`. Always `ls /home/ccuser/opsman-work/Jobs/` first — subfolder names contain spaces ("Ongoing Jobs") and must be quoted in all shell commands.
8. **Always `ls` a directory before reading files inside it.** On the VPS vault, `07d - Daily Diary/` and `07a - Dockets/` are **directories** (not files).
9. **Job folder names have spaces:** "Ongoing Jobs" and "01-2275 - HPE Field of Play Hornsby" both have spaces. Always quote paths in shell commands when they contain spaces.
10. **Job-Tracker.md is empty (zero rows):** This is a real finding — report "no entries logged today" explicitly. Do not skip the hours/materials section or imply a diary exists when it doesn't.

- [WhatsApp-Site-Diary Architecture](./references/whatsapp-site-diary-architecture.md) — WhatsApp → Hermes → Airtable diary loop. No Zapier. Pilot: Hornsby HPE Field of Play. Pre-reqs, config file locations, PAT security, approval-gate workflow, open questions.
- [Site Diary Operations](./references/site-diary-operations.md) — End-to-end Daily Record recipe: Airtable POST body, vault file pattern, Photos field quirk (it's `multilineText`, not attachment), "vision dead" workflow fallback.
- [WhatsApp Bridge Dual-State](./references/whatsapp-bridge-dual-state.md) — bridge.log loop with no listener, empty `openclaw channels list`, full session folder. Safe relink protocol preserving old session.
- [Variation — 48-Hour Rule](./references/variation-48hr-rule.md) — TfNSW/tier-1 VO notice deadlines, workflow advantage with Hermes, what to capture in the VO of LFCS cron jobs with job IDs, schedule, status, deliver target. Update this file when jobs are created/paused/resumed/deleted so future sessions have the current state without needing to call cronjob list.
- [Cron Smoke Test](./references/cron-smoke-test.md) — protocol for live-testing any read-only register cron skill (e.g. pm-heartbeat) before arming schedule. Failure-mode pattern + reusable Airtable snippets.
- [Morning Digest Google Fetch](./references/morning-digest-google-fetch.md) — Gmail/Calendar fetch layer for the 05:00 morning-digest cron. Token-refresh-on-401 policy, Python-subprocess fanout pattern, `***`-placeholder hook survival, calendar 401/Gmail 200 split handling, tirith/quoting traps. **There is no standalone `lfcs-morning-digest` skill — the cron prompt references this file directly under the `lfcs` umbrella.**
- [VPS Cron Script Traps](./references/vps-cron-traps.md) — hermes-health-snapshot.sh fixes: flock guard, safe .env parse (no set -a), correct MiniMax endpoint (api.minimax.io), pm2 jlist vs pm2 list hang, heredoc mv-before-chown pattern. Also: Google OAuth token expiry check and re-auth command. Dashboard auth verify pattern (curl -I → 302 /login). Cron job registry (5 jobs, IDs, statuses, next runs).
- [Vision Troubleshoot](./references/vision-troubleshoot.md) — AUXILIARY_VISION_API_KEY fix, native vs aux mode, MiniMax vision, OpenAI key swap
- [Weekly Programme Snapshot](./references/weekly-programme-snapshot.md) — Monday cron: week N, float, work due, claims due, RFIs to chase, VOs pending, TBT due, induction expiry check

- [Local Artefact Recovery](./references/local-artefact-recovery.md) — SSH retrieval from laptop when job not in vault
- [TCE Liverpool Recovery](./references/tce-liverpool-recovery.md) — worked example of laptop-closed gap protocol, identifier mismatch, questions to ask
- [EOD Google Doc Report](./references/eod-google-doc-report.md) — compile daily report with photos, upload to Google Drive, insert inline images, share link
- [Stormwater Pit Rates](./references/stormwater-pit-rates.md) — benchmark breakdown, TCE Livingstone actuals, 2626 Mack Civil, 2626 Hasibul full-scope ($107k), adjustment rules
- [Site Visit Prep](./references/site-visit-prep.md) — address, contact, key risks, questions to ask. Load before Rocky heads to a job site.
- [Telegram Channel Binding](./references/telegram-channel-binding.md) — procedure for binding a new job's Telegram group to Hermes (`getMe` → phone-side directory add → `getUpdates` → dry-run ping → Airtable bind) plus the failure modes that hit the 2636 Liverpool bind (double blue ticks lie, `joinChat` 404s on invite hashes, optimistic UI service messages, privacy-mode gotcha, **`getUpdates` single-consumer conflict**, OpenClaw polling regression).
- [Telegram Bot Health Check](./scripts/telegram-bot-health-check.sh) — safe outbound probes (`getMe`, `sendMessage`, `getChatMember`, `getChat`) that do NOT consume the gateway's long-poll slot. Use instead of raw `getUpdates` when debugging bot connectivity.
- [New Job Intake From Photos](./references/new-job-intake-photos.md) — when Rocky sends site photos + a casual reference ("2649 cr plus", "the TCE job"). Airtable-first protocol, phonetic-slip patterns, photo-vs-scope sanity check.
- [Docket Check](./references/docket-check.md) — signed docket verification, at-risk revenue flags
- [EOD Summary](./references/eod-summary.md) — end-of-day report procedure and format
- [Job Tracker](./references/job-tracker.md) — mid-day status, budget variance
- [Pre-Pour Checklist](./references/pre-pour-check.md) — 8-item concrete pour walkthrough
- [Concrete Pricing](./references/concrete-pricing.md) — 40MPa/80mm slump/20mm agg specs, Sydney readymix rates, short load surcharge rules, plant call checklist
- [RFI Status](./references/rfi-status.md) — open RFI register, priority sorting, chase actions
- [Client Correspondence](./references/client-correspondence.md) — email/letter drafting templates, tone rules, filing
- [Tool & Equipment Tracker](./references/tool-tracker.md) — system design, WhatsApp workflow, Google Sheets schema
- [Weekly Timesheet](./references/timesheet.md) — hours compilation and submission workflow
- [Morning Digest Google Fetch](./references/morning-digest-google-fetch.md) — Google OAuth token refresh (tirith-safe), Gmail list+metadata fanout, Calendar 7-day window, telegram-length cap, **cron-loop Bash quoting pitfall (2026-07-08)**, **`***`-hook scope limitation (2026-07-22)**
- [Morning Digest Cron Template](./templates/morning-digest-digest.py) — canonical single-file Python cron script (refresh-on-401, urllib, no `***` markers). Copy to /tmp/digest.py and adapt.
- [Morning Digest Gotchas 2026-07-13](./references/morning-digest-gotchas-2026-07-13.md) — urlencode-list empty-headers trap, Calendar dedup of same meeting from different organisers, top-5 unread editorial rules (1-per-thread, promo filter, tender platforms always surface)
- [Morning Digest Cron Delivery](./references/morning-digest-cron-delivery.md) — the cron auto-delivers to the configured target; don't call `hermes send` to the same target from inside the job. Token-expiry probe pattern + tirith emoji-variation-selector gotcha.
- [Morning Digest Gmail Headers 2026-07-18](./references/morning-digest-gmail-headers-2026-07-18.md) — `format=metadata` strips `payload.headers` (use default format=full instead); google_token.json has no `expiry_date` (probe 401 → refresh); Calendar API host is `www.googleapis.com` not `gmail.googleapis.com`.

The user has provided the following instruction alongside the skill invocation: [IMPORTANT: You are running as a scheduled cron job. DELIVERY: Your final response will be automatically delivered to the user — do NOT use send_message or try to deliver the output yourself. Just produce your report/output as your final response and the system handles the rest. SILENT: If there is genuinely nothing new to report, respond with exactly "[SILENT]" (nothing else) to suppress delivery. Never combine [SILENT] with content — either report your findings normally, or say [SILENT] and nothing more.]

You are Hermes, Rocky's site ops agent. Sydney timezone. Deliver a morning intelligence digest to Rocky's Telegram (chat 7377499346) via send_message.

Gmail + Google Calendar only (do NOT check LFCS email — that's handled separately). Use the Google OAuth token at ~/.hermes/google_token.json. If the token is expired or invalid, send this message instead: "⚠️ Morning digest unavailable — Google OAuth token needs re-auth. Run: python3 /usr/local/lib/hermes-agent/setup.py --auth-url"

**Format — tight bullets, deadlines first, no filler:**

```
🗓 [DAY] Morning Digest — [DD MMM]

## Deadlines today
(bullets)

## Action items
(bullets)

## Gmail — top 5 unread
(bullets: sender, subject, snippet, time)

## Calendar — next 7 days
(bullets: time, event, location if any)

## Waiting on (3+ days, from open follow-ups in memory)
(read ~/.hermes/memories/MEMORY.md, find open follow-ups, include items with no reply/activity in 3+ days — if none, write "Nothing stale")
```

Scopes to request: gmail.readonly + calendar (already in token). Max 5 email items. Keep snippets to 1 line each. If token is valid, actually fetch real data — do not fabricate emails or calendar events.

## Response

🗓 Friday Morning Digest — 24 Jul

## Deadlines today
- **16:00** — Timesheet due to Hass (admin@lfcs) — week ending

## Action items
- Marshall follow-up: re-bar / settlement / slab rectifications needed after yesterday's footpath pour (2676 Liverpool) — awaiting reply
- RFT Concrete FRP Works — Wentworth Point CC4: HSEQ questionnaire returned, awaiting next step

## Gmail — top 5 unread
- **Admin Team** (23 Jul 13:24) — Re: RFT - Concrete FRP Works — Ford Civil Wentworth Point CC4 — *HSEQ Questionnaire attached*
- **Admin Team** (23 Jul 11:08) — Re: East and North Footpath pour — *David, clarification on curing — not in scope, rec is Concrete…*
- **David Marshall** (23 Jul 10:45) — RE: East and North Footpath pour — *Everything went well yesterday, small set…*
- **Admin Team** (23 Jul 10:24) — East and North Footpath pour — *Massive effort yesterday, shoutout to Solutions+*
- **Google Cloud** (23 Jul 08:12) — Enable 2-step verification by Oct 20 — *MY CONSOLE*

## Calendar — next 7 days
- **Fri 24 Jul 16:00** — Timesheet → Hass (admin@lfcs)

## Waiting on (3+ days, open follow-ups)
- Nothing stale

⚠️ File-mutation verifier: 1 file(s) were NOT modified this turn despite any wording above that may suggest otherwise. Run `git status` or `read_file` to confirm.
  • `/tmp/digest_fetch.py` — [patch] Could not find a match for old_string in the file Did you mean one of these sections? 6| # Load token 7| t = json.load(open('`/root/.hermes/google_token.json`')) 8| TOKEN = t['acces…
