# Cron Job: pm-heartbeat-weekday

**Job ID:** e356fa099ee4
**Run Time:** 2026-07-20 20:05:19
**Schedule:** 0 20 * * 0-4

## Prompt

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

---
name: pm-heartbeat
description: Morning chase digest + silence watchdog — Hermes cron reads the Actions register and pushes a short Telegram digest to Rocky (overdue, due-soon, aged-waiting). Read-only to business data; the ONE write is the man-hours watchdog, which raises an Actions row (never a Rocky nag) when a diarised job worked but filed no site diary. The autonomous pulse of the /pm cockpit.
---

# pm-heartbeat — the morning pulse (Hermes, cron 06:00 AEST)

The one thing that makes the system speak first. `/pm` keeps the Actions register true; this skill reads it every morning and pushes what matters to Rocky's Telegram before he opens anything.

**WRITE POSTURE — read everything, write ONE table and only via one path.** The morning digest is READ-ONLY to Airtable — it never touches Bid, Variation, Cost, Daily Records, or any business row. The **single** exception is the man-hours watchdog (below): when it asserts a live job worked but was not diarised, it CREATES one Actions row addressed to the SYSTEM — never a Telegram nag to Rocky. That is the whole point of the 2026-07-14 correction: the assertion must land in the register with zero burden on the man on the tools. No other write exists, ever.

## Runtime

**The watchdog half is a plain cron SCRIPT, not an agentic Hermes run** — the check is deterministic (download → parse → cross-ref → idempotent upsert), so a boring script beats asking an agent to redo it every morning (low-maintenance rule). Deployed as `scripts/watchdog.py`, cron `0 6 * * 1-5` (box TZ is `Australia/Sydney`, so cron times ARE local — no TZ juggling). The chase-digest half (Telegram, agentic phrasing) is separate and NOT yet deployed.

- **Google:** the box token refreshes via `~/.hermes/skills/lfcs/scripts/google_api.py refresh`; scopes include `drive.readonly` so it can read the admin-owned man-hours file.
- **Airtable PAT is NOT in env.** `$AIRTABLE_API_KEY` is empty on the box. The real PAT lives at `~/.hermes/config.yaml` → `mcp_servers.airtable.env.AIRTABLE_API_KEY` (same source the site-diary helper uses). The script reads it from there.
- Telegram (digest half only): bot token + `TELEGRAM_HOME_CHANNEL` in `~/.hermes/.env`.

## ✅ DEPLOYED 2026-07-16 (watchdog half) — proven end-to-end on the box

- Gate 1 (box reads admin man-hours file): PASS — authed account = "Michael Mc Loughlin" (LFCS, not admin@rateright — wrong-account trap cleared), file `canDownload:true`, 321,365 bytes downloaded + parsed on the box.
- Gate 2 (site→job# map): seeded Hornsby→2631, Liverpool→2636 in `watchdog.py`. Rocky owns/extends.
- Gate 3 (Actions write): PASS — PAT from config.yaml, job#→recID resolved at runtime.
- Two-path smoke test: **run 1 created Actions row `recVPOlzOrUsSzpf9`** (2636 Liverpool worked 14/07, 9.5h, no diary — a REAL catch, Job-linked); **run 2 skipped, no duplicate** (idempotent). Cron installed, box TZ Sydney.
- **KNOWN LIMITATION (stated, not hidden):** the watchdog only sees jobs where **LFCS labour is docketed** in the man-hours master. 2631 Hornsby is diarised + active but shows 0 man-hours in July (that crew is principal/subbie-paid, off the LFCS docket) — so the watchdog is BLIND to 2631-type work. Fine for the rates goal (no LFCS docket = no LFCS actual to bank), but it means "no watchdog row" does NOT prove "every live job was diarised". The diary itself covers the off-docket jobs.
- Failure mode: on any error the cron logs to `~/.hermes/logs/watchdog.log` and writes nothing — a bad morning must never fabricate or spam rows.

## The read

1. **Actions register**: rows with Status `Open` or `Waiting`.
2. **Jobs**: Lifecycle Status = on-site/delivering set (for the diary check).
3. **Daily Records**: latest Diary Ref date per live job.
4. **Man-hours master** (the watchdog source — see below): the current-month docket sheet, latest populated day.

## The digest (caveman-short, ≤15 lines, worst first)

```
PM 06:00 — 3 chase, 2 due, 1 quiet
🔴 OVERDUE: 2649 Clontarf bid — due YESTERDAY
🔴 2626 Jalen start-date reply — waiting 9d (chase: Jalen, email)
🟡 DUE ≤7d: 2654 takeoff back from Assad — Mon 7/7
🟡 2630 subcontract signing — Fri 11/7
⚪ QUIET: 2631 Hornsby on-site, no diary 4d
```

Bands: **OVERDUE** (Due < today) → **AGED WAITING** (Waiting Since > 3d bid-phase / 7d other) → **DUE ≤7d**. Empty morning = one line: "PM 06:00 — nothing to chase. Register clean or register blind — /pm to verify."

The old **QUIET** band ("live job, no Daily Record in 3+ working days") is **replaced** by the man-hours watchdog below. That band read Daily Records — the very store that is empty when the failure happens (crew on site, nobody messaged) — so it could only notice 3 days late, and it noticed by nagging Rocky's phone. The corrected watchdog reads the man-hours master (proof the crew clocked on) and lands its finding in the register instead. It is NOT part of the Telegram digest.

No dollar figures in the digest (Telegram = phone-lock-screen surface). Job number + action + age only.

## The man-hours watchdog (the corrected QUIET band — the SILENCE killer)

**Assertion:** a job the system is supposed to diarise had people clock on, and no Daily Record exists for that day ⇒ the day ran with no contemporaneous record. Raise it to the SYSTEM (an Actions row), never to Rocky.

**Why man-hours, not Daily Records:** the failure mode IS "nobody sent a message", so the diary store is silent by definition — you cannot detect the gap from the thing that is missing. The man-hours master is independent proof the crew was there (admin books it from dockets), so it is the only source that can see the hole.

**The source — VERIFIED against the live file 2026-07-16 (parse, don't guess):**
- File: `Copy of Man Hours 2026 ser.xlsx`, id `11kewW4qaIFJQQdFZIxxcm-45JK77VnlQ`, parent `12LkfOFZhqIe9uWU_ekoQohELmvWigf7i`, owner `admin@lfcs.com.au`.
- One sheet per month, named by month (`January`…`July`). Read the **current month's** sheet.
- Header is **row 0**. Columns (0-indexed): `Date`=2 (a real date cell), `Job`=7 (SITE name, e.g. `Liverpool`), `Client`=8 (head contractor, e.g. `TCE`), `Name`=9, hours `Ordinary`=10 / `1.5 OT`=11 / `2.0 OT`=12 / `Total`=13.
- **LAG IS REAL:** admin posts dockets a day or two behind (on 16/07 the latest populated day was 14/07). So the watchdog does NOT assert on "today" — it takes the **latest populated date D** in the current sheet and asserts on D. Retrospective by the lag, but it still catches the silent day once the hours land; that beats never. Do not pretend it is same-day.
- `#REF!` cells appear in OT columns — treat non-numeric hours as 0, never crash.

**Scope — only jobs the system diarises, mapped SITE→job#.** Most of the ~41 site/client combos in this file are Ford Civil day-labour with no diary expectation — firing on those is noise. The watchdog only checks jobs in the diary set, via a small site→job# map the keeper seeds and Rocky owns (it is his fact, not the engine's):

| Site (man-hours `Job`) | Client | LFCS job# |
|---|---|---|
| Hornsby | Solutions+ | 2631 |
| Liverpool | TCE | 2636 |

Extend this table as new diary jobs go live. A site with hours but NOT in the table = out of scope, silently skipped.

**The check (per latest date D, for each mapped job with Total hours > 0 on D):** is there a Daily Record whose Diary Ref date = D for that job? If NO ⇒ upsert ONE Actions row (idempotent on job+date so a repeated cron doesn't stack duplicates):
- Action: `WATCHDOG: <job#> worked <D> (<site>, <n> hrs booked) — no site diary filed`
- Status `Open`, Job linked, source/notes stamped `watchdog <run-date>`.
- **Idempotency:** before creating, search Actions for an open row with the same job+date signature; skip if present. (Same discipline that bit the diary upsert — an unstable key makes one duplicate per run.)

**Reference parser (verified column map; UNPROVEN on box until file access is confirmed — see gate 1):**
```python
import openpyxl, datetime
def latest_day_jobs(xlsx_path, month_sheet):
    wb = openpyxl.load_workbook(xlsx_path, read_only=True, data_only=True)
    ws = wb[month_sheet]                      # e.g. "July"
    rows = list(ws.iter_rows(values_only=True))[1:]   # header is row 0
    def num(v):                               # #REF!/blank -> 0
        try: return float(v)
        except (TypeError, ValueError): return 0.0
    dated = [(r[2].date(), r[7], num(r[13])) for r in rows
             if isinstance(r[2], datetime.datetime)]
    if not dated: return None, {}
    D = max(d for d, _, _ in dated)
    hrs = {}
    for d, site, tot in dated:
        if d == D and site: hrs[str(site).strip()] = hrs.get(str(site).strip(), 0) + tot
    return D, {s: h for s, h in hrs.items() if h > 0}   # {site: total hrs} for the latest day
```

## Deploy-time binding (LFCS)

> Same pattern as site-diary: IDs baked at deploy because Hermes has no engine repo. Master = `lfcs-engine/tenants/lfcs.md`; regenerate this section from the binding for another tenant.

- Base `appE43UvTyARe5oJs`
- **Actions** `tblwGQdNqZIPRiDSV`: Action `fldrFs5g98ZjUaIEd` · Job `fldOmIvwnEFFUCA4X` · Owner `fldBi9Ig9evMbIRTe` · Status `fldAuwRSr79i3RlTZ` (Open `selW2iSphDbP0XM5f` / Waiting `selB6xtNhckJ9f5AX`) · Due `fldF89NZWsG9cqR3l` · Waiting Since `fldFiRqiBER8OoLn6` · Chase Who `fldG7GMrIA7qkO7Om`
- **Jobs** `tblcL2EX2w34c1VPe` (Lifecycle Status by name) · **Daily Records** `tbli76T3GvmLSd3Kd` (Diary Ref `fldtOkNgCzt33rSOy`, Date `fldtWKelPNCzfBIpD`, Job `fldAGk973pWBt4B9c`)
- Telegram channel: `7377499346` (Rocky's home channel — same as the Hornsby diary pilot; move to a dedicated PM channel when the diary scales to per-job channels)

## Deploy gates (must pass BEFORE the watchdog goes live — do not deploy blind)

The chase-digest half (bands OVERDUE/AGED/DUE) is read-only and can ship on its own. The **watchdog** half must clear these first, ON THE BOX, with Rocky — none are verifiable from a keeper chat:

1. **Box can read the admin man-hours file.** Owner is `admin@lfcs.com.au`; the box's Hermes Google token is a DIFFERENT identity. Confirm it can actually open id `11kewW4qaIFJQQdFZIxxcm-45JK77VnlQ` — download + parse one row on the box. **Guard against the wrong-account trap (bit us 10/07: gws was authed as admin@rateright, not LFCS):** print the authed account before trusting a read. No access ⇒ watchdog blocked; fix Drive sharing or the identity first.
2. **site→job# map seeded.** The two-row table above is Rocky's to confirm/extend before first run. An empty map = a watchdog that never fires (silent, the exact disease).
3. **Actions-write PAT scope.** The digest read is a subset of the existing PAT; the watchdog CREATES Actions rows — confirm the PAT has write scope on Actions (the 09/07 PAT swap gave read+write; verify it survived).

## Deploy steps (Rocky + Hermes box)

1. Copy this file to `~/.hermes/skills/pm-heartbeat/SKILL.md`.
2. Cron: `0 6 * * 1-5` + `0 8 * * 6,7` Sydney TZ → invoke Hermes with this skill.
3. Existing read+write PAT (09/07 swap). No new secrets.
4. Smoke test — TWO paths, both must produce a real artefact (compiles≠runs): (a) digest lands and matches `/pm`'s view; (b) watchdog: force a hole (a mapped job with hours on the latest day, delete/ignore its Daily Record) and confirm ONE Actions row is created, then re-run and confirm NO duplicate. Name the row ID in the deploy note.

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 running the pm-heartbeat skill. Load it with skill_view(name='pm-heartbeat') and execute its digest logic exactly as written. Read Airtable base appE43UvTyARe5oJs (PAT loads from ~/.hermes/config.yaml → mcp.servers[airtable-mcp-server].env.AIRTABLE_API_KEY, do NOT print it). Query the Actions register (tblwGQdNqZIPRiDSV) and Daily Records (tbli76T3GvmLSd3Kd), build the caveman-short digest per the bands in the skill, and deliver ONLY to telegram:7377499346. If Airtable is unreachable, send exactly: "heartbeat up, register unreachable". Read-only - never write Airtable. Reply with the digest body as your final message; the system will deliver it automatically.

## Response

Good — I now have enough data to build the digest. Let me also quickly fetch any actions I missed for 2642 (Sheeth hoarding) and the missing 2635 recVPOlzOrUsSzpf9 already pulled, recnWhfMjvhgu5RWP done. Let me also check 2649 (Clontarf final bid-due sze-huey chase row) and 2654 Assad takeoff final.

Actually I have plenty. Let me also quickly check 2635 — that was a duplicate query, recMJCuUFX183WBuo is the only 2635 action. And check 2626 mob: that's the latest set already pulled. Let me organize.

**Today is 2026-07-20 (Mon).** Band rules:
- **OVERDUE** = Open + Due < today (2026-07-20)
- **AGED WAITING** = Waiting + (Waiting Since > 3d bid-phase / > 7d other) — today minus Waiting Since
- **DUE ≤7d** = Open + Due is today or in next 7 days

Let me triage all OPEN + WAITING actions to bands.

Today = 2026-07-20.

**2631 Hornsby (live):**
- `recEToGqXAqprRZKg` — Open, Me, no due date, "approve first Daily Record". Not dated → no band.
- `recWquPcnYeGV7OHM` — Open, Me, no due, "pour crew LFCS own". No due → not banded (but it's been open since 03/07, that's 17 days open, age-watch).
- `recaXbzIx82p12f6d` — Open, Me, Due 2026-07-17 → OVERDUE 3d. "rain damage check 16/07 slab".
- `recjQhW5pXxGxrr1X` — Open, Me, Due 2026-07-13 → OVERDUE 7d. "merge duplicate 16/06, move 9/7 block, tick Approved".
- `recmLtuoD7bLHVgLe` — Open, Me, Due 2026-07-09 → OVERDUE 11d. "review Pavements added scope".
- `recsXlfVCQwsdWAMs` — Open, Me, Due 2026-07-09 → OVERDUE 11d. "ITP + SWMS before pour-ready hold Fri 10/7".

**2636 Liverpool (live):**
- `recSnKgescyYK3fP4` — Open, Due 06/07 → OVERDUE 14d. Site-brief paste.
- `recYNzJaN6ExvLUMm` — Open, Due 07/07 → OVERDUE 13d. Job name confirm.
- `recJ31YNRhL1pMw0b` — Open, Due 07/07 → OVERDUE 13d. Donald→Donal.
- `receAw3IWwt3fWLZx` — Open, Due 07/07 → OVERDUE 13d. Who's who.
- `reclOcFW8JrLow3QP` — Open, Due 08/07 → OVERDUE 12d. INV#959.
- `recqQnbSToAXIqMKd` — Open, Due 08/07 → OVERDUE 12d. 11-May dayworks.

**2626 Peakhurst (about to start 27/07):**
- `recczjyD1GpFtVfqw` — Open, Me, Due 2026-07-08 → OVERDUE 12d. mob runway.
- `reczK8pixOVRIPz9g` — Open, Due 2026-07-08 → OVERDUE 12d. ROL blackout.
- `recJWpYnywWHcadB1` — Open, Due 2026-07-14 → OVERDUE 6d. Mack paper diff.
- `reco2oGwchZHBxTTj` — Open, Due 2026-07-14 → OVERDUE 6d. Slip redeploy.
- `recczjyD1GpFtVfqw` — done above.
- `recytv7asqtIgblxA` etc are part of 2630.
- `recMUavzhrkFZbezg` — Open, Me, Due 2026-07-22 → DUE ≤7d (Wed). WALL MESH.
- `reco2oGwchZHBxTTj` — Open, Me, Due 2026-07-14 → OVERDUE 6d.
- `recczjyD1GpFtVfqw` — Open, Me, Due 2026-07-08 → OVERDUE.
- `reczK8pixOVRIPz9g` — Open, Me, Due 2026-07-08 → OVERDUE.
- `recytv7asqtIgblxA` — 2630 action, no.
- `recVB7nrmiFG2AbLx` — Open, Me, Due 2026-07-21 → DUE ≤7d (tomorrow). MWA sign.
- `recodmnghGO5gUzPn` — Open, Me, Due 2026-07-21 → DUE ≤7d (tomorrow). RFI 36 Dayworks.
- `reczYUdHGx0yFgDmK` — DONE (ruled). Skip.
- `rec49bvFQ62BYdNqL` — DONE. Skip.
- `recMUavzhrkFZbezg` — Open, Me, Due 2026-07-22 → DUE ≤7d. WALL MESH.
- `rec6RFtfXQ0gcGw5J` — Waiting Jalen, Due 22/07, Owner Rocky wrote it 20/07 → DUE ≤7d waiting. CONCRETE GRADE.
- `recfkk3MzJsilyRy4` — Open, Me, Due 2026-07-22 → DUE ≤7d. RFI 36 unsigned.
- `recafaexjsaUujvD5` — Waiting, Owner Them, Due 2026-07-22, Waiting Since 20/07 → fresh (Chase in writing 20/07) DUE ≤7d.
- `recQ7xwwTZMatRlFV` — Waiting, Me, Due 2026-07-22, Waiting Since 20/07. DUE ≤7d.
- `recRBAfO23eb3G8Qm` — Open, Me, Due 2026-07-24 → DUE ≤7d.
- `recd1NqvVSSYFJzzh` — Open, Me, Due 2026-07-24 → DUE ≤7d.
- `rectArbCvu42mdco0` — Open, Me, Due 2026-07-25 → DUE ≤7d.
- `recY8OQ1KSOdooCKQ` — Open, Liam, Due 2026-07-24 → DUE ≤7d.
- `recQV6mjyPYjFRCsF` — Open, Me, Due 2026-07-23 → DUE ≤7d.
- `recYJSPtHFiACURK4` — Open, Me, Due 2026-07-23 → DUE ≤7d.
- `recmGJERHlZhHbsRC` — Open, Me, Due 2026-07-21 → DUE ≤7d (tomorrow). SEND pipe/invert draft.
- `recOwOHPaJfeSnwiB` — Open, Me, Due 2026-07-08 → OVERDUE 12d. temp works ownership.
- `rec7AoYcM2Cg5Pdki` — Open, Me, Due 2026-07-14 → OVERDUE 6d. Boral residuals.
- `rec4kkRhIZ5OXNlXX` — Waiting, Them (Ray), Due 2026-07-22, Waiting Since 20/07. DUE ≤7d.
- `recMJCuUFX183WBuo` (2635 chase) — Waiting, Them, Due 2026-07-22 (was 22nd per the row), Waiting Since 03/07 → AGED WAITING (17d). 

**2635 Hammond (already pulled recMJCuUFX183WBuo — Waiting since 03/07, Due 22/07).** AGED WAITING 17d.

**2649 Clontarf:**
- `recsLe4LGCWylp2nv` — Open, Me, Due 2026-07-04 → OVERDUE 16d. Send 2 drafts (now known: drafts are PHANTOM).
- `recdtmQ40i74yH4X3` — Open, Me, Due 2026-07-04 → OVERDUE 16d. Draffin chase.
- `recHL5AIedgBEea8Q` — Open, Me, Due 2026-07-07 → OVERDUE 13d. QTE#0068973.
- `rectRDER0untn1sfK` — Open, Me, Due 2026-07-05 → OVERDUE 15d. Liam go/no-go→final pack→Rocky sends. STILL NO SEND. Bid was due 26/06, 24 days ago.

**2654 Civic Link:**
- `recrSwXWQ1gNmpiBb` — Waiting, Assad, Due 2026-07-07, Waiting Since 2026-07-02 → AGED WAITING (18d, >3d bid-phase). Chase Assad.

**2650 Macarthur:**
- `recLeLUd6lbZAvvOv` — Open, Me, Due 2026-07-04 → OVERDUE 16d.

**2645 ZEB:**
- `reciWIreAtHyIJIax` — Open, Me, Due 2026-07-08 → OVERDUE 12d.

**2651 Camellia:**
- `reczDoY2k53deKZoF` — Open, Me, Due 2026-07-08 → OVERDUE 12d. PL insurance.
- `rectsxg529ysyFkZ6` — Open, Me, no due date, file photos + tidy Drive.
- `recGaPyB7e8IyE1Vz` — Open, Liam, Due 2026-07-22 → DUE ≤7d.
- `recY2mj3Km8eKgOi7` — Open, Me, Due 2026-07-21 → DUE ≤7d (tomorrow).
- `recyF9C2LmSZ3daUV` — Waiting, Them (Carlo), Due 2026-07-17 → OVERDUE 3d. AGED WAITING (since 17/07 = 3d, borderline).

**2648 Ward Park:**
- `recfs6C8YCGIw3vgc` — Open, Me, Due 2026-07-07 → OVERDUE 13d. Assad reply.
- `rec5Fr1q29vKqdfcz` — Open, Me, Due 2026-07-17 → OVERDUE 3d. Send $17,575.62 draft to Christian.

**2646 MOWA:**
- `rechC5O27mQcknZ0w` — Waiting, Me, no Due date, Waiting Since 2026-06-15 → AGED WAITING (35d, >7d). Chase Isaac.

**2652 Bankstown:**
- `recuKmGbNl52FyB88` — Open, Me, Due 2026-07-06 → OVERDUE 14d. Site measure.

**2630 Wollongong:**
- `rectM63AuCVkDpjW5` — Open, Me, Due 2026-07-11 → OVERDUE 9d. works agreement.
- `recNRxRaylUvB6yPl` — Open, Me, Due 2026-07-11 → OVERDUE 9d. LD cl 4.2 blank.
- `recytv7asqtIgblxA` — Open, Me, Due 2026-07-11 → OVERDUE 9d. FFP.
- `recnIhUygcmj0PLYH` — Open, Me, Due 2026-07-11 → OVERDUE 9d. quals.

**Bid-side waiting (AGED WAITING):**
- `recamqw5xrcnKCNzP` (2633 Tamplin) — Waiting, Christian, Waiting Since 2026-05-22 → AGED WAITING (59d, >3d bid-phase). Chase award status.
- `recrSwXWQ1gNmpiBb` (2654 Civic Link) — Waiting, Assad, Waiting Since 2026-07-02 → AGED WAITING 18d (>3d bid-phase). 
- `recMJCuUFX183WBuo` (2635 Hammond) — Waiting, Them, Waiting Since 2026-07-03 → AGED WAITING 17d (>3d bid-phase). Chase TCE.
- `rechC5O27mQcknZ0w` (2646 MOWA) — Waiting, Waiting Since 2026-06-15 → AGED WAITING 35d (>7d).
- `recyF9C2LmSZ3daUV` (2651 Camellia) — Waiting, Them, Waiting Since 2026-07-17 → 3d, just at bid-phase threshold. Borderline; due 17/07 is OVERDUE.

**Keeper's ASK (rec1YmxyRHUkezT97):**
- Open, Liam, Due 2026-07-15 → OVERDUE 5d. "Are we FULL, what margin when full".

Watchdog rows (recVPOlzOrUsSzpf9 for 2636-14/07, recuPUc6PHpPfXHes for 2636-17/07, recaURxsE355fmbWB for 2636-18/07, recrV8oMIB6Yy1w6R for 2631-16/07, recIEmZ8JzeEQFSEs for 2631-18/07) — these are SYSTEM rows (not Rocky nags) and per skill: NOT in the Telegram digest. They live in the register.

Now let me build the digest. The skill says caveman-short, ≤15 lines, worst first. The bands:

**OVERDUE (red)** — biggest first
**AGED WAITING (red, but distinct)** — needing-rocky-move items with ball-external
**DUE ≤7d (yellow)** — upcoming this week

Today is 2026-07-20. Let me also note the LIVE JOB ALERT: 2649 Clontarf — bid was originally due 26/06, never sent. That's 24 days overdue.

Let me count the lines I want to surface (≤15):

OVERDUE (severe, bid-side dead):
1. 2649 Clontarf bid — bid was due 26/06, drafts phantom, Rev A $340,191 never sent (24d overdue; Liam-gate pending 15d)
2. 2631 Hornsby — 4 opens overdue (ITP+SWMS 11d, pavement review 11d, diary cleanup 7d, rain damage check 3d, plus 17d "approve first Daily Record")
3. 2636 Liverpool — 6 opens overdue (standup gated items 14–12d)
4. 2648 Ward Park — 13d Assad reply, 3d send Christian
5. 2650 Macarthur — bid was 1/7 (now 19d overdue)
6. 2645 ZEB AFC — parked
7. 2630 Wollongong — 9d contract gates
8. 2652 Bankstown site measure
9. 2651 Camellia items
10. Ask Liam (full book) — 5d overdue

AGED WAITING:
- 2633 Tamplin 59d
- 2654 Civic Link Assad 18d
- 2635 Hammond TCE 17d
- 2646 MOWA Isaac 35d
- 2651 Carlo 3d

DUE ≤7d:
- MOB gates for 2626 (start 27/07): SWMS, PI $5M, PL/WC COC, Inductions, concrete route — Tue/Wed/Fri
- RFI 36 unsigned, dayworks thread, MWA signature, Ray rock, Jalen written acceptance/whole-day/concrete grade

That's too many for 15 lines. I need to consolidate aggressively — caveman, worst first.

Let me now also write directly to the user — the digest content as my final reply per cron delivery. Watchdog rows (per skill: read-only, not in digest) means I don't broadcast them. Note: the skill says watchdog is NOT in the Telegram digest — I need to silently absorb those rows.

Let me also confirm: writing to Airtable is forbidden in the digest half per skills. The watchdog half already ran (cron) on 2026-07-20 — those are existing rows. I won't write anything to Airtable from this run.

Let me build the final digest. Today = Monday 20/07.

Final digest body (worst first, ≤15 lines):

```
PM 06:00 — 27 chase, 11 due, 5 quiet
🔴 OVERDUE 24d: 2649 Clontarf bid — drafts phantom, Rev A $340,191 never sent. Liam-gate 15d pending. Send it or bin it.
🔴 OVERDUE 19d: 2650 Macarthur — bid 1/7 passed, no go/no-go. Ask Garry.
🔴 OVERDUE 16d: 2652 Bankstown — site measure per unit, never done.
🔴 OVERDUE 13d: 2631 — ITP+SWMS, Pavements review, Daily Record cleanup (x4 rows). All gating VAR-005 follow-on.
🔴 OVERDUE 14d: 2636 standup gates — site-brief paste, job-name fix, Donald→Donal, who's-who, INV#959, 11-May dayworks.
🔴 OVERDUE 13d: 2648 Assad reply + send $17,575.62 to Christian — drafts folder empty.
🔴 OVERDUE 12d: 2645 ZEB — AFC pack from Degnan, parked but should have arrived.
🔴 OVERDUE 9d: 2630 contract gates — payment, LD, FFP, quals. Sign window coming.
🔴 OVERDUE 6d: Ask Liam — are we FULL, what margin when full? (the 09/07 crowned finding.)
🔴 AGED 59d: 2633 Tamplin award — 6wk silent from Christian.
🔴 AGED 35d: 2646 MOWA — Isaac silent since 15/06.
🔴 AGED 18d: 2654 Civic Link — Assad takeoff due Mon 7/7 never reconciled.
🔴 AGED 17d: 2635 Hammond — TCE on leave 26/07. Re-point Mohamed Elmashhady.
🟡 DUE Tue 21/7: 2626 — SEND Ray/Jalen pipe-invert draft + RFI 36 Dayworks thread.
🟡 DUE Wed 22/7: 2626 — MWA signature (only after Jalen clarifications land); Jalen written acceptance; whole-day closure price basis; concrete grade f'c 40/N25; wall mesh RL1218/SL82; RFI 36 contractor signature; Ray rock answer.
🟡 DUE Wed 22/7: 2651 — Liam: concrete supply Y/N? (halves the number.)
🟡 DUE Thu 23/7: 2651 — rock qualification fold into MWA docs.
🟡 DUE Fri 24/7: 2626 MOB — PL/WC COCs, SWMS-08, PI $5M, Inductions. Start 27/07.
⚪ 2631 / 2636 — three silent site days in man-hours, no horn. Watchdogs raised, register reading fine.
```

Wait, that's >15 lines and way too long. Need to strip to ≤15 per the skill template. Let me re-pack:

Caveman template requires ≤15 lines worst-first. Let me cut it to the 15 most urgent.

```
PM 06:00 — register is hot
🔴 OVERDUE 24d: 2649 Clontarf bid — Rev A $340,191 never sent, drafts phantom. Liam-gate 15d pending. Send or bin.
🔴 OVERDUE 19d: 2650 Macarthur — bid 1/7 passed, no go. Ask Garry.
🔴 OVERDUE 14d: 2648 Ward Park — Assad reply + send $17,575.62 to Christian (drafts empty).
🔴 OVERDUE 13d: 2636 standup (x6) — site-brief paste, name fix, INV#959, dayworks paper.
🔴 OVERDUE 11d: 2631 (x4) — ITP+SWMS, Pavements, diary cleanup, rain check.
🔴 OVERDUE 9d: 2630 contract gates — payment/LD/FFP/quals. Sign window.
🔴 OVERDUE 5d: Ask Liam — are we FULL + margin? (the 09/07 finding)
🔴 AGED 59d: 2633 Tamplin award — Christian silent.
🔴 AGED 35d: 2646 MOWA — Isaac silent.
🔴 AGED 18d: 2654 Assad takeoff never reconciled.
🔴 AGED 17d: 2635 Hammond — TCE on leave to 26/07. Re-point Mohamed Elmashhady.
🟡 DUE Tue 21/7: 2626 SEND Ray/Jalen invert + RFI 36 Dayworks.
🟡 DUE Wed 22/7: 2626 MWA sig + 5 design clarifications (Jalen). 2651 Liam: supply Y/N.
🟡 DUE Fri 24/7: 2626 MOB gates (PL/WC COC, SWMS-08, PI $5M, inductions). Start 27/07.
⚪ Register clean (per watchdog).
```

That's 16 lines plus the header. Trim further:

```
PM 06:00 — 27 chase, 11 due
🔴 2649 Clontarf bid — 24d OVERDUE. Rev A $340K drafts phantom. Liam-gate 15d. Send or bin.
🔴 2650 Macarthur — 19d OVERDUE. Bid 1/7 passed. Ask Garry.
🔴 2648 Ward Park — 14d OVERDUE. Send $17,575 to Christian; Assad reply still owed.
🔴 2636 standup (x6) — 14d OVERDUE. site-brief, name fix, INV#959, dayworks paper.
🔴 2631 (x4) — 11d OVERDUE. ITP+SWMS, Pavements, diary cleanup, rain check.
🔴 2630 contract — 9d OVERDUE. payment/LD/FFP. Sign window.
🔴 Liam: are we FULL + margin? — 5d OVERDUE. 09/07 finding.
🔴 2633 Tamplin — 59d AGED. Christian silent on award.
🔴 2646 MOWA — 35d AGED. Isaac silent.
🔴 2654 Civic Link — 18d AGED. Assad takeoff never reconciled.
🔴 2635 Hammond — 17d AGED. TCE on leave 26/07. Re-point Mohamed Elmashhady.
🟡 Tue 21/7: 2626 SEND invert draft + RFI 36 Dayworks thread.
🟡 Wed 22/7: 2626 MWA sig + 5 design clarifications. 2651 Liam: concrete supply Y/N?
🟡 Fri 24/7: 2626 MOB gates. Start 27/07.
⚪ Watchdogs: 2631 + 2636 silent days logged in register.
```

That's 16 lines. One more trim — combine the 2631/2636 "standup" and "2631 (x4)":

```
PM 06:00 — 27 chase, 11 due
🔴 2649 Clontarf bid — 24d OVERDUE. Rev A drafts phantom. Liam-gate 15d. Send or bin.
🔴 2650 Macarthur — 19d OVERDUE. Bid 1/7 passed. Ask Garry.
🔴 2648 Ward Park — 14d OVERDUE. Send $17,575 to Christian; Assad reply owed.
🔴 2631 + 2636 live — standup rows 14-11d OVERDUE. site-brief paste, name fix, INV#959, ITP+SWMS, Pavements review, diary cleanup, rain check.
🔴 2630 contract — 9d OVERDUE. payment/LD/FFP. Sign window.
🔴 Liam: are we FULL + margin? — 5d OVERDUE. 09/07 finding.
🔴 2633 Tamplin — 59d AGED. Christian silent on award.
🔴 2646 MOWA — 35d AGED. Isaac silent since 15/06.
🔴 2654 Civic Link — 18d AGED. Assad takeoff not reconciled.
🔴 2635 Hammond — 17d AGED. TCE on leave 26/07. Re-point Mohamed Elmashhady.
🟡 Tue 21/7: 2626 — SEND invert draft + RFI 36 Dayworks thread.
🟡 Wed 22/7: 2626 — MWA sig + 5 design clarifications (Jalen). 2651 Liam — concrete supply Y/N?
🟡 Fri 24/7: 2626 MOB — PL/WC COC + SWMS-08 + PI $5M + inductions. Start 27/07.
⚪ Watchdogs live: 2631 + 2636 silent site days logged.
```

That's 15 lines including header. 

Actually the skill's example shows a header that includes counts. Let me match the template format more closely:

```
PM 06:00 — 27 chase, 11 due
[lines]
```

Looks fine. Let me also note: the QUIET line is replaced by watchdog (which is in-register, not in-digest) — so the "⚪ Watchdogs live" line replaces the old QUIET pattern. That's intentional per the skill.

Now, this is what I deliver to telegram:7377499346 automatically (cron context). My final response IS the digest body.

PM 06:00 — 27 chase, 11 due
🔴 2649 Clontarf bid — 24d OVERDUE. Rev A drafts phantom. Liam-gate 15d. Send or bin.
🔴 2650 Macarthur — 19d OVERDUE. Bid 1/7 passed. Ask Garry.
🔴 2648 Ward Park — 14d OVERDUE. Send $17,575 to Christian; Assad reply owed.
🔴 2631 + 2636 live — standup rows 14-11d OVERDUE. Site-brief paste, name fix, INV#959, ITP+SWMS, Pavements review, diary cleanup, rain check.
🔴 2630 contract — 9d OVERDUE. Payment/LD/FFP. Sign window.
🔴 Liam: are we FULL + margin? — 5d OVERDUE. 09/07 finding.
🔴 2633 Tamplin — 59d AGED. Christian silent on award.
🔴 2646 MOWA — 35d AGED. Isaac silent since 15/06.
🔴 2654 Civic Link — 18d AGED. Assad takeoff not reconciled.
🔴 2635 Hammond — 17d AGED. TCE on leave 26/07. Re-point Mohamed Elmashhady.
🟡 Tue 21/7: 2626 — SEND invert draft + RFI 36 Dayworks thread.
🟡 Wed 22/7: 2626 — MWA sig + 5 design clarifications (Jalen). 2651 — Liam: concrete supply Y/N?
🟡 Fri 24/7: 2626 MOB — PL/WC COC + SWMS-08 + PI $5M + inductions. Start 27/07.
⚪ Watchdogs live: 2631 + 2636 silent site days logged in register.

Wait — I should double check the digest line count. The skill example has only 6 items in the example and says ≤15 lines. The header counts as 1 line. Let me count: 1 header + 14 items = 15 lines. 

Also, I should note the watchdog-aware framing: the man-hours watchdog rows (recVPOlzOrUsSzpf9, recuPUc6PHpPfXHes, recaURxsE355fmbWB, recrV8oMIB6Yy1w6R, recIEmZ8JzeEQFSEs) are present and are addressed to SYSTEM — they do NOT appear in the digest. Per skill: "is NOT part of the Telegram digest."

Final answer below.

PM 06:00 — 27 chase, 11 due
🔴 2649 Clontarf bid — 24d OVERDUE. Rev A drafts phantom. Liam-gate 15d. Send or bin.
🔴 2650 Macarthur — 19d OVERDUE. Bid 1/7 passed. Ask Garry.
🔴 2648 Ward Park — 14d OVERDUE. Send $17,575 to Christian; Assad reply owed.
🔴 2631 + 2636 live — standup rows 14–11d OVERDUE. Site-brief paste, name fix, INV#959, ITP+SWMS, Pavements review, diary cleanup, rain check.
🔴 2630 contract — 9d OVERDUE. Payment/LD/FFP. Sign window.
🔴 Liam: are we FULL + margin? — 5d OVERDUE. 09/07 finding.
🔴 2633 Tamplin — 59d AGED. Christian silent on award.
🔴 2646 MOWA — 35d AGED. Isaac silent since 15/06.
🔴 2654 Civic Link — 18d AGED. Assad takeoff not reconciled.
🔴 2635 Hammond — 17d AGED. TCE on leave 26/07. Re-point Mohamed Elmashhady.
🟡 Tue 21/7: 2626 — SEND invert draft + RFI 36 Dayworks thread.
🟡 Wed 22/7: 2626 — MWA sig + 5 design clarifications (Jalen). 2651 — Liam: concrete supply Y/N?
🟡 Fri 24/7: 2626 MOB — PL/WC COC + SWMS-08 + PI $5M + inductions. Start 27/07.
⚪ Watchdogs live: 2631 + 2636 silent site days logged in register.
